react-native-ai-devtools 1.4.27 → 1.4.29

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.
@@ -1480,6 +1480,350 @@ export async function getScreenLayout(options = {}) {
1480
1480
  }
1481
1481
  return result;
1482
1482
  }
1483
+ export async function getPressableElements(options = {}) {
1484
+ const { device } = options;
1485
+ // --- Step 1: walk fiber tree, find pressable/input elements, dispatch measureInWindow ---
1486
+ const dispatchExpression = `
1487
+ (function() {
1488
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1489
+ if (!hook) return { error: 'React DevTools hook not found.' };
1490
+
1491
+ var roots = [];
1492
+ if (hook.getFiberRoots) {
1493
+ roots = Array.from(hook.getFiberRoots(1) || []);
1494
+ }
1495
+ if (roots.length === 0 && hook.renderers) {
1496
+ for (var entry of hook.renderers) {
1497
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
1498
+ if (r.length > 0) { roots = r; break; }
1499
+ }
1500
+ }
1501
+ if (roots.length === 0) return { error: 'No fiber roots found.' };
1502
+
1503
+ function getMeasurable(fiber) {
1504
+ var sn = fiber.stateNode;
1505
+ if (!sn) return null;
1506
+ if (typeof sn.measureInWindow === 'function') return sn;
1507
+ if (sn.canonical && sn.canonical.publicInstance &&
1508
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1509
+ return sn.canonical.publicInstance;
1510
+ }
1511
+ return null;
1512
+ }
1513
+
1514
+ function findFirstHost(fiber, depth) {
1515
+ if (!fiber || depth > 20) return null;
1516
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
1517
+ var child = fiber.child;
1518
+ while (child) {
1519
+ var found = findFirstHost(child, depth + 1);
1520
+ if (found) return found;
1521
+ child = child.sibling;
1522
+ }
1523
+ return null;
1524
+ }
1525
+
1526
+ function collectText(fiber, d) {
1527
+ if (!fiber || d > 30) return '';
1528
+ var props = fiber.memoizedProps;
1529
+ if (props) {
1530
+ var ch = props.children;
1531
+ if (typeof ch === 'string') return ch;
1532
+ if (typeof ch === 'number') return String(ch);
1533
+ if (Array.isArray(ch)) {
1534
+ var inline = [];
1535
+ for (var ci = 0; ci < ch.length; ci++) {
1536
+ if (typeof ch[ci] === 'string') inline.push(ch[ci]);
1537
+ else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
1538
+ }
1539
+ if (inline.length > 0) return inline.join('');
1540
+ }
1541
+ }
1542
+ var parts = [];
1543
+ var child = fiber.child;
1544
+ while (child) {
1545
+ var t = collectText(child, d + 1);
1546
+ if (t) parts.push(t);
1547
+ child = child.sibling;
1548
+ }
1549
+ return parts.join(' ').trim();
1550
+ }
1551
+
1552
+ function getComponentName(fiber) {
1553
+ if (!fiber || !fiber.type) return null;
1554
+ if (typeof fiber.type === 'string') return fiber.type;
1555
+ return fiber.type.displayName || fiber.type.name || null;
1556
+ }
1557
+
1558
+ 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)$/;
1559
+
1560
+ var hostFibers = [];
1561
+ var fiberMeta = [];
1562
+
1563
+ function findMeaningfulAncestorName(fiber) {
1564
+ var cur = fiber.return;
1565
+ var depth = 0;
1566
+ var fallbackName = null;
1567
+ while (cur && depth < 20) {
1568
+ var name = getComponentName(cur);
1569
+ if (name && typeof cur.type !== 'string') {
1570
+ if (!fallbackName) fallbackName = name;
1571
+ if (!RN_PRIMITIVES.test(name)) return name;
1572
+ }
1573
+ cur = cur.return;
1574
+ depth++;
1575
+ }
1576
+ return fallbackName;
1577
+ }
1578
+
1579
+ function buildPath(fiber) {
1580
+ var parts = [];
1581
+ var cur = fiber;
1582
+ var depth = 0;
1583
+ while (cur && depth < 30) {
1584
+ var name = getComponentName(cur);
1585
+ if (name && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(name)) {
1586
+ parts.unshift(name);
1587
+ }
1588
+ cur = cur.return;
1589
+ depth++;
1590
+ }
1591
+ // Keep last 3 segments
1592
+ if (parts.length > 3) {
1593
+ parts = parts.slice(-3);
1594
+ return '... > ' + parts.join(' > ');
1595
+ }
1596
+ return parts.join(' > ');
1597
+ }
1598
+
1599
+ function walkPressables(fiber, depth) {
1600
+ if (!fiber || depth > 5000) return;
1601
+ var name = getComponentName(fiber);
1602
+ var props = fiber.memoizedProps;
1603
+
1604
+ // Skip inactive/unfocused screens
1605
+ if (name === 'MaybeScreen' && props && props.active === 0) return;
1606
+ if (name === 'SceneView' && props && props.focused === false) return;
1607
+ if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
1608
+
1609
+ var isPressable = props && typeof props.onPress === 'function';
1610
+ var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1611
+
1612
+ if (isPressable || isInput) {
1613
+ var host = findFirstHost(fiber, 0);
1614
+ if (host) {
1615
+ var text = collectText(fiber, 0);
1616
+ var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
1617
+ var path = buildPath(fiber);
1618
+ var testID = (props && (props.testID || props.nativeID)) || null;
1619
+ var accessibilityLabel = (props && props.accessibilityLabel) || null;
1620
+
1621
+ hostFibers.push(host);
1622
+ fiberMeta.push({
1623
+ component: componentName,
1624
+ path: path,
1625
+ text: text ? text.slice(0, 100) : '',
1626
+ testID: testID,
1627
+ accessibilityLabel: accessibilityLabel,
1628
+ isInput: !!isInput
1629
+ });
1630
+ }
1631
+ }
1632
+
1633
+ var child = fiber.child;
1634
+ while (child) {
1635
+ walkPressables(child, depth + 1);
1636
+ child = child.sibling;
1637
+ }
1638
+ }
1639
+
1640
+ walkPressables(roots[0].current, 0);
1641
+
1642
+ if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
1643
+
1644
+ // Also measure the root view for viewport detection
1645
+ var rootHost = findFirstHost(roots[0].current, 0);
1646
+ if (rootHost) {
1647
+ hostFibers.unshift(rootHost);
1648
+ fiberMeta.unshift({ component: '__root__', path: '', text: '', testID: null, accessibilityLabel: null, isInput: false });
1649
+ }
1650
+
1651
+ globalThis.__pressableFibers = hostFibers;
1652
+ globalThis.__pressableMeta = fiberMeta;
1653
+ globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
1654
+
1655
+ for (var i = 0; i < hostFibers.length; i++) {
1656
+ try {
1657
+ (function(idx) {
1658
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1659
+ globalThis.__pressableMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1660
+ });
1661
+ })(i);
1662
+ } catch(e) {}
1663
+ }
1664
+
1665
+ return { count: hostFibers.length };
1666
+ })()
1667
+ `;
1668
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
1669
+ if (!dispatchResult.success)
1670
+ return dispatchResult;
1671
+ try {
1672
+ const parsed = JSON.parse(dispatchResult.result || "{}");
1673
+ if (parsed.error)
1674
+ return { success: false, error: parsed.error };
1675
+ }
1676
+ catch {
1677
+ /* ignore */
1678
+ }
1679
+ // Wait for measureInWindow callbacks
1680
+ await delay(300);
1681
+ // --- Step 2: read measurements, filter visible, build results ---
1682
+ const resolveExpression = `
1683
+ (function() {
1684
+ var fibers = globalThis.__pressableFibers;
1685
+ var meta = globalThis.__pressableMeta;
1686
+ var measurements = globalThis.__pressableMeasurements;
1687
+ globalThis.__pressableFibers = null;
1688
+ globalThis.__pressableMeta = null;
1689
+ globalThis.__pressableMeasurements = null;
1690
+
1691
+ if (!fibers || !measurements || !meta) {
1692
+ return { error: 'No measurement data. Run get_pressable_elements again.' };
1693
+ }
1694
+
1695
+ // Get viewport dimensions
1696
+ 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;
1703
+ }
1704
+ }
1705
+
1706
+ var elements = [];
1707
+
1708
+ for (var i = 0; i < measurements.length; i++) {
1709
+ var m = measurements[i];
1710
+ if (!m) continue;
1711
+
1712
+ var info = meta[i];
1713
+ // Skip the root viewport measurement entry
1714
+ if (info.component === '__root__') continue;
1715
+
1716
+ // Filter: only visible within viewport
1717
+ if (m.width <= 0 || m.height <= 0) continue;
1718
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
1719
+ if (m.x > viewportW || m.y > viewportH) continue;
1720
+
1721
+ var text = info.text || '';
1722
+
1723
+ elements.push({
1724
+ component: info.component,
1725
+ path: info.path,
1726
+ center: {
1727
+ x: Math.round(m.x + m.width / 2),
1728
+ y: Math.round(m.y + m.height / 2)
1729
+ },
1730
+ frame: {
1731
+ x: Math.round(m.x),
1732
+ y: Math.round(m.y),
1733
+ width: Math.round(m.width),
1734
+ height: Math.round(m.height)
1735
+ },
1736
+ text: text,
1737
+ testID: info.testID,
1738
+ accessibilityLabel: info.accessibilityLabel,
1739
+ hasLabel: text.length > 0,
1740
+ isInput: info.isInput
1741
+ });
1742
+ }
1743
+
1744
+ // Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
1745
+ // often share the same frame. Keep the one with the most meaningful component name.
1746
+ var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
1747
+ var deduped = {};
1748
+ for (var di = 0; di < elements.length; di++) {
1749
+ var el = elements[di];
1750
+ var key = el.frame.x + ',' + el.frame.y + ',' + el.frame.width + ',' + el.frame.height;
1751
+ var existing = deduped[key];
1752
+ if (!existing) {
1753
+ deduped[key] = el;
1754
+ } else {
1755
+ // Prefer non-host/non-primitive name
1756
+ var existingIsGeneric = HOST_NAMES.test(existing.component);
1757
+ var newIsGeneric = HOST_NAMES.test(el.component);
1758
+ 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;
1764
+ }
1765
+ }
1766
+ }
1767
+ elements = [];
1768
+ for (var dk in deduped) {
1769
+ elements.push(deduped[dk]);
1770
+ }
1771
+
1772
+ // Sort top-to-bottom, left-to-right
1773
+ elements.sort(function(a, b) {
1774
+ if (a.center.y !== b.center.y) return a.center.y - b.center.y;
1775
+ return a.center.x - b.center.x;
1776
+ });
1777
+
1778
+ var iconCount = 0;
1779
+ var labeledCount = 0;
1780
+ for (var j = 0; j < elements.length; j++) {
1781
+ if (elements[j].hasLabel) labeledCount++;
1782
+ else iconCount++;
1783
+ }
1784
+
1785
+ return {
1786
+ pressableElements: elements,
1787
+ summary: 'Found ' + elements.length + ' pressable elements (' + iconCount + ' icon-only, ' + labeledCount + ' with text labels)'
1788
+ };
1789
+ })()
1790
+ `;
1791
+ const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
1792
+ if (result.success && result.result) {
1793
+ try {
1794
+ const parsed = JSON.parse(result.result);
1795
+ if (parsed.error)
1796
+ return { success: false, error: parsed.error };
1797
+ const pressableElements = parsed.pressableElements || [];
1798
+ // Format as readable text
1799
+ const lines = [parsed.summary, ""];
1800
+ for (let i = 0; i < pressableElements.length; i++) {
1801
+ const el = pressableElements[i];
1802
+ const num = i + 1;
1803
+ const label = el.hasLabel ? `"${el.text}"` : "(icon/image)";
1804
+ const ids = [];
1805
+ if (el.testID)
1806
+ ids.push(`testID="${el.testID}"`);
1807
+ if (el.accessibilityLabel)
1808
+ ids.push(`a11y="${el.accessibilityLabel}"`);
1809
+ const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
1810
+ 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}`);
1812
+ if (el.path)
1813
+ lines.push(` path: ${el.path}`);
1814
+ }
1815
+ return {
1816
+ success: true,
1817
+ result: lines.join("\n"),
1818
+ parsedElements: pressableElements
1819
+ };
1820
+ }
1821
+ catch {
1822
+ // If parsing fails, return original result
1823
+ }
1824
+ }
1825
+ return result;
1826
+ }
1483
1827
  /**
1484
1828
  * Format enriched elements as an indented tree with tap coordinates in pixels.
1485
1829
  * Same tree structure as get_screen_layout but with tap:(x,y) per node.
@@ -1944,7 +2288,7 @@ export async function findComponents(pattern, options = {}) {
1944
2288
  * Matches by text content, testID, or component name.
1945
2289
  */
1946
2290
  export async function pressElement(options) {
1947
- const { text, testID, component, index = 0, maxTraversalDepth = 15 } = options;
2291
+ const { text, testID, component, index = 0, maxTraversalDepth = 15, device } = options;
1948
2292
  if (!text && !testID && !component) {
1949
2293
  return { success: false, error: "At least one of text, testID, or component must be provided." };
1950
2294
  }
@@ -1952,19 +2296,20 @@ export async function pressElement(options) {
1952
2296
  const textParam = text ? `'${esc(text)}'` : "null";
1953
2297
  const testIDParam = testID ? `'${esc(testID)}'` : "null";
1954
2298
  const componentParam = component ? `'${esc(component)}'` : "null";
1955
- const expression = `
2299
+ // --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
2300
+ const dispatchExpression = `
1956
2301
  (function() {
1957
2302
  var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1958
2303
  if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
1959
2304
 
1960
2305
  var roots = [];
1961
2306
  if (hook.getFiberRoots) {
1962
- var renderers = hook.renderers;
1963
- if (renderers) {
1964
- for (var entry of renderers) {
1965
- var r = Array.from(hook.getFiberRoots(entry[0]) || []);
1966
- if (r.length > 0) { roots = r; break; }
1967
- }
2307
+ roots = Array.from(hook.getFiberRoots(1) || []);
2308
+ }
2309
+ if (roots.length === 0 && hook.renderers) {
2310
+ for (var entry of hook.renderers) {
2311
+ var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
2312
+ if (r.length > 0) { roots = r; break; }
1968
2313
  }
1969
2314
  }
1970
2315
  if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
@@ -1973,6 +2318,7 @@ export async function pressElement(options) {
1973
2318
  var searchTestID = ${testIDParam};
1974
2319
  var searchComponent = ${componentParam};
1975
2320
  var targetIndex = ${index};
2321
+ var maxTraversalUp = ${maxTraversalDepth};
1976
2322
 
1977
2323
  function getComponentName(fiber) {
1978
2324
  if (!fiber || !fiber.type) return null;
@@ -2003,15 +2349,63 @@ export async function pressElement(options) {
2003
2349
  return parts.join('');
2004
2350
  }
2005
2351
 
2006
- var matches = [];
2352
+ function getMeasurable(fiber) {
2353
+ var sn = fiber.stateNode;
2354
+ if (!sn) return null;
2355
+ if (typeof sn.measureInWindow === 'function') return sn;
2356
+ if (sn.canonical && sn.canonical.publicInstance &&
2357
+ typeof sn.canonical.publicInstance.measureInWindow === 'function') {
2358
+ return sn.canonical.publicInstance;
2359
+ }
2360
+ return null;
2361
+ }
2007
2362
 
2008
- function walkFiber(fiber, path) {
2009
- if (!fiber) return;
2363
+ // Find the first measurable host descendant of a fiber.
2364
+ // For inputs, prefer TextInput-specific hosts over generic RCTView.
2365
+ function findFirstHost(fiber, depth, isInput) {
2366
+ if (!fiber || depth > 20) return null;
2367
+ if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
2368
+ if (isInput) {
2369
+ var hostType = typeof fiber.type === 'string' ? fiber.type : '';
2370
+ if (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1) {
2371
+ return fiber;
2372
+ }
2373
+ }
2374
+ return fiber;
2375
+ }
2376
+ var child = fiber.child;
2377
+ var fallback = null;
2378
+ while (child) {
2379
+ var found = findFirstHost(child, depth + 1, isInput);
2380
+ if (found) {
2381
+ if (isInput) {
2382
+ var ft = typeof found.type === 'string' ? found.type : '';
2383
+ if (ft.indexOf('TextInput') !== -1 || ft.indexOf('textinput') !== -1) {
2384
+ return found;
2385
+ }
2386
+ if (!fallback) fallback = found;
2387
+ } else {
2388
+ return found;
2389
+ }
2390
+ }
2391
+ child = child.sibling;
2392
+ }
2393
+ return fallback;
2394
+ }
2395
+
2396
+ var hostFibers = [];
2397
+ var tapMeta = [];
2398
+
2399
+ // Phase 1: Walk the entire tree, collect all pressable/input elements
2400
+ function walkFiber(fiber, depth, path) {
2401
+ if (!fiber || depth > 5000) return;
2010
2402
  var name = getComponentName(fiber);
2011
2403
  var props = fiber.memoizedProps;
2012
2404
 
2013
- // Skip off-screen subtrees — React Navigation marks hidden screens with aria-hidden
2405
+ // Skip inactive/hidden screens
2014
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;
2015
2409
 
2016
2410
  var isPressable = props && typeof props.onPress === 'function';
2017
2411
  var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
@@ -2021,33 +2415,24 @@ export async function pressElement(options) {
2021
2415
  if (isPressable) {
2022
2416
  text = extractText(fiber, 0);
2023
2417
  } else {
2024
- // For inputs: use child text, value, defaultValue, or placeholder
2025
2418
  var val = typeof props.value === 'string' ? props.value : '';
2026
2419
  var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
2027
2420
  var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
2028
2421
  text = extractText(fiber, 0) || val || defVal || ph;
2029
2422
  }
2030
2423
  var tid = props.testID || props.nativeID || null;
2031
- var matched = true;
2032
-
2033
- if (searchText !== null) {
2034
- matched = matched && text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
2035
- }
2036
- if (searchTestID !== null) {
2037
- matched = matched && (tid === searchTestID);
2038
- }
2039
- if (searchComponent !== null) {
2040
- matched = matched && (name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
2041
- }
2042
2424
 
2043
- if (matched) {
2044
- matches.push({
2045
- fiber: fiber,
2425
+ var host = findFirstHost(fiber, 0, isInput);
2426
+ if (host) {
2427
+ hostFibers.push(host);
2428
+ tapMeta.push({
2046
2429
  name: name || '(anonymous)',
2047
2430
  text: text.substring(0, 100),
2048
2431
  testID: tid,
2049
2432
  path: path.join(' > '),
2050
- isInput: isInput
2433
+ isInput: isInput,
2434
+ isPressable: isPressable,
2435
+ source: 'direct'
2051
2436
  });
2052
2437
  }
2053
2438
  }
@@ -2055,151 +2440,254 @@ export async function pressElement(options) {
2055
2440
  var child = fiber.child;
2056
2441
  while (child) {
2057
2442
  var childName = getComponentName(child);
2058
- walkFiber(child, childName ? path.concat([childName]) : path);
2443
+ walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
2059
2444
  child = child.sibling;
2060
2445
  }
2061
2446
  }
2062
2447
 
2063
2448
  for (var ri = 0; ri < roots.length; ri++) {
2064
- walkFiber(roots[ri].current, []);
2449
+ walkFiber(roots[ri].current, 0, []);
2450
+ }
2451
+
2452
+ // Phase 2a: testID on non-pressable wrapper — walk UP to pressable/input parent
2453
+ if (searchTestID !== null) {
2454
+ var hasDirectTestIDMatch = false;
2455
+ for (var di = 0; di < tapMeta.length; di++) {
2456
+ if (tapMeta[di].testID === searchTestID) { hasDirectTestIDMatch = true; break; }
2457
+ }
2458
+
2459
+ if (!hasDirectTestIDMatch) {
2460
+ function findByTestID2a(fiber, path) {
2461
+ if (!fiber) return;
2462
+ var name = getComponentName(fiber);
2463
+ 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;
2467
+
2468
+ var tid = props && (props.testID || props.nativeID || null);
2469
+ if (tid === searchTestID) {
2470
+ var nIsPressable = props && typeof props.onPress === 'function';
2471
+ var nIsInput = !nIsPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
2472
+
2473
+ if (nIsPressable || nIsInput) {
2474
+ var text = nIsPressable ? extractText(fiber, 0) : (extractText(fiber, 0) || (typeof props.value === 'string' ? props.value : '') || (typeof props.defaultValue === 'string' ? props.defaultValue : '') || (typeof props.placeholder === 'string' ? props.placeholder : ''));
2475
+ var host = findFirstHost(fiber, 0, nIsInput);
2476
+ if (host) {
2477
+ hostFibers.push(host);
2478
+ tapMeta.push({
2479
+ name: name || '(anonymous)',
2480
+ text: text.substring(0, 100),
2481
+ testID: searchTestID,
2482
+ path: path.join(' > '),
2483
+ isInput: nIsInput,
2484
+ isPressable: nIsPressable,
2485
+ source: 'testID-direct'
2486
+ });
2487
+ }
2488
+ } else {
2489
+ var parent = fiber.return;
2490
+ var d = 0;
2491
+ while (parent && d < maxTraversalUp) {
2492
+ var pp = parent.memoizedProps;
2493
+ var pIsPressable = pp && typeof pp.onPress === 'function';
2494
+ var pIsInput = !pIsPressable && pp && (typeof pp.onChangeText === 'function' || typeof pp.onFocus === 'function');
2495
+ if (pIsPressable || pIsInput) {
2496
+ var pText = pIsPressable ? extractText(parent, 0) : (extractText(parent, 0) || (typeof pp.value === 'string' ? pp.value : '') || (typeof pp.defaultValue === 'string' ? pp.defaultValue : '') || (typeof pp.placeholder === 'string' ? pp.placeholder : ''));
2497
+ var host = findFirstHost(parent, 0, pIsInput);
2498
+ if (host) {
2499
+ hostFibers.push(host);
2500
+ tapMeta.push({
2501
+ name: name || '(anonymous)',
2502
+ text: pText.substring(0, 100),
2503
+ testID: pp.testID || pp.nativeID || searchTestID,
2504
+ path: path.join(' > '),
2505
+ isInput: pIsInput,
2506
+ isPressable: pIsPressable,
2507
+ source: 'testID-ancestor'
2508
+ });
2509
+ }
2510
+ break;
2511
+ }
2512
+ parent = parent.return;
2513
+ d++;
2514
+ }
2515
+ }
2516
+ }
2517
+ var child = fiber.child;
2518
+ while (child) {
2519
+ var childName = getComponentName(child);
2520
+ findByTestID2a(child, childName ? path.concat([childName]) : path);
2521
+ child = child.sibling;
2522
+ }
2523
+ }
2524
+ for (var ri2a = 0; ri2a < roots.length; ri2a++) {
2525
+ findByTestID2a(roots[ri2a].current, []);
2526
+ }
2527
+ }
2065
2528
  }
2066
2529
 
2067
- // Phase 2a: If searching by testID and no match found,
2068
- // the testID may be on a non-pressable/non-input wrapper — search ALL nodes
2069
- // for the testID and walk UP to find nearest pressable or input ancestor
2070
- if (matches.length === 0 && searchTestID !== null) {
2071
- var testIDCandidates = [];
2072
-
2073
- function findByTestID(fiber, path) {
2074
- if (!fiber) return;
2075
- var name = getComponentName(fiber);
2076
- var props = fiber.memoizedProps;
2077
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
2530
+ // Phase 2b: component name on non-pressable node — walk UP to pressable parent
2531
+ if (searchComponent !== null) {
2532
+ var hasDirectComponentMatch = false;
2533
+ 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;
2536
+ }
2537
+ }
2078
2538
 
2079
- var tid = props && (props.testID || props.nativeID || null);
2080
- if (tid === searchTestID) {
2081
- testIDCandidates.push({ fiber: fiber, name: name || '(anonymous)', path: path.join(' > ') });
2539
+ if (!hasDirectComponentMatch) {
2540
+ function findByName2b(fiber, path) {
2541
+ if (!fiber) return;
2542
+ var name = getComponentName(fiber);
2543
+ 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;
2547
+
2548
+ if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
2549
+ var parent = fiber.return;
2550
+ var d = 0;
2551
+ while (parent && d < maxTraversalUp) {
2552
+ var pp = parent.memoizedProps;
2553
+ if (pp && typeof pp.onPress === 'function') {
2554
+ var text = extractText(parent, 0);
2555
+ var host = findFirstHost(parent, 0, false);
2556
+ if (host) {
2557
+ hostFibers.push(host);
2558
+ tapMeta.push({
2559
+ name: name,
2560
+ text: text.substring(0, 100),
2561
+ testID: pp.testID || pp.nativeID || null,
2562
+ path: path.join(' > '),
2563
+ isInput: false,
2564
+ isPressable: true,
2565
+ source: 'component-ancestor'
2566
+ });
2567
+ }
2568
+ break;
2569
+ }
2570
+ parent = parent.return;
2571
+ d++;
2572
+ }
2573
+ }
2574
+ var child = fiber.child;
2575
+ while (child) {
2576
+ var childName = getComponentName(child);
2577
+ findByName2b(child, childName ? path.concat([childName]) : path);
2578
+ child = child.sibling;
2579
+ }
2082
2580
  }
2083
- var child = fiber.child;
2084
- while (child) {
2085
- var childName = getComponentName(child);
2086
- findByTestID(child, childName ? path.concat([childName]) : path);
2087
- child = child.sibling;
2581
+ for (var ri2b = 0; ri2b < roots.length; ri2b++) {
2582
+ findByName2b(roots[ri2b].current, []);
2088
2583
  }
2089
2584
  }
2585
+ }
2090
2586
 
2091
- for (var ri3 = 0; ri3 < roots.length; ri3++) {
2092
- findByTestID(roots[ri3].current, []);
2093
- }
2587
+ if (hostFibers.length === 0) {
2588
+ var criteria = [];
2589
+ if (searchText !== null) criteria.push('text="' + searchText + '"');
2590
+ if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
2591
+ if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
2592
+ return { error: 'No pressable or focusable elements found. Searched for: ' + criteria.join(', ') };
2593
+ }
2094
2594
 
2095
- // For each candidate, first check if the node itself is pressable/input,
2096
- // then walk UP to find nearest pressable/input ancestor
2097
- var maxDepthTID = ${maxTraversalDepth};
2098
- for (var ti = 0; ti < testIDCandidates.length; ti++) {
2099
- var tCandidate = testIDCandidates[ti];
2100
- var tProps = tCandidate.fiber.memoizedProps;
2101
- var tIsPressable = tProps && typeof tProps.onPress === 'function';
2102
- var tIsInput = !tIsPressable && tProps && (typeof tProps.onChangeText === 'function' || typeof tProps.onFocus === 'function');
2595
+ // Store host fibers and metadata globally for step 2, dispatch measureInWindow
2596
+ globalThis.__tapHostFibers = hostFibers;
2597
+ globalThis.__tapMeta = tapMeta;
2598
+ globalThis.__tapMeasurements = new Array(hostFibers.length).fill(null);
2103
2599
 
2104
- if (tIsPressable || tIsInput) {
2105
- var text = extractText(tCandidate.fiber, 0);
2106
- if (tIsInput) {
2107
- var val = typeof tProps.value === 'string' ? tProps.value : '';
2108
- var defVal = typeof tProps.defaultValue === 'string' ? tProps.defaultValue : '';
2109
- var ph = typeof tProps.placeholder === 'string' ? tProps.placeholder : '';
2110
- text = text || val || defVal || ph;
2111
- }
2112
- matches.push({
2113
- fiber: tCandidate.fiber,
2114
- name: tCandidate.name,
2115
- text: text.substring(0, 100),
2116
- testID: searchTestID,
2117
- path: tCandidate.path,
2118
- isInput: tIsInput
2600
+ for (var mi = 0; mi < hostFibers.length; mi++) {
2601
+ try {
2602
+ (function(idx) {
2603
+ getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
2604
+ globalThis.__tapMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
2119
2605
  });
2120
- continue;
2121
- }
2606
+ })(mi);
2607
+ } catch(e) {}
2608
+ }
2122
2609
 
2123
- // Walk up to find pressable/input ancestor
2124
- var tParent = tCandidate.fiber.return;
2125
- var tDepth = 0;
2126
- while (tParent && tDepth < maxDepthTID) {
2127
- var tParentProps = tParent.memoizedProps;
2128
- var parentIsPressable = tParentProps && typeof tParentProps.onPress === 'function';
2129
- var parentIsInput = !parentIsPressable && tParentProps && (typeof tParentProps.onChangeText === 'function' || typeof tParentProps.onFocus === 'function');
2130
-
2131
- if (parentIsPressable || parentIsInput) {
2132
- var text = extractText(tParent, 0);
2133
- if (parentIsInput) {
2134
- var val = typeof tParentProps.value === 'string' ? tParentProps.value : '';
2135
- var defVal = typeof tParentProps.defaultValue === 'string' ? tParentProps.defaultValue : '';
2136
- var ph = typeof tParentProps.placeholder === 'string' ? tParentProps.placeholder : '';
2137
- text = text || val || defVal || ph;
2138
- }
2139
- matches.push({
2140
- fiber: tParent,
2141
- name: tCandidate.name,
2142
- text: text.substring(0, 100),
2143
- testID: tParentProps.testID || tParentProps.nativeID || searchTestID,
2144
- path: tCandidate.path,
2145
- isInput: parentIsInput
2146
- });
2147
- break;
2148
- }
2149
- tParent = tParent.return;
2150
- tDepth++;
2151
- }
2610
+ return { count: hostFibers.length };
2611
+ })()
2612
+ `;
2613
+ const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
2614
+ if (!dispatchResult.success)
2615
+ return dispatchResult;
2616
+ try {
2617
+ const parsed = JSON.parse(dispatchResult.result || "{}");
2618
+ if (parsed.error)
2619
+ return { success: false, error: parsed.error };
2620
+ }
2621
+ catch {
2622
+ /* ignore */
2623
+ }
2624
+ // Wait for measureInWindow callbacks
2625
+ await delay(300);
2626
+ // --- Step 2: Read measurements, filter visible, match by query ---
2627
+ const resolveExpression = `
2628
+ (function() {
2629
+ var hostFibers = globalThis.__tapHostFibers;
2630
+ var meta = globalThis.__tapMeta;
2631
+ var measurements = globalThis.__tapMeasurements;
2632
+ globalThis.__tapHostFibers = null;
2633
+ globalThis.__tapMeta = null;
2634
+ globalThis.__tapMeasurements = null;
2635
+
2636
+ if (!hostFibers || !measurements || !meta) {
2637
+ return { error: 'No measurement data. Dispatch step may have failed.' };
2638
+ }
2639
+
2640
+ var searchText = ${textParam};
2641
+ var searchTestID = ${testIDParam};
2642
+ var searchComponent = ${componentParam};
2643
+ var targetIndex = ${index};
2644
+
2645
+ // Determine viewport bounds
2646
+ var viewportW = 9999, viewportH = 9999;
2647
+ for (var v = 0; v < measurements.length; v++) {
2648
+ if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
2649
+ measurements[v].width > 0 && measurements[v].height > 0) {
2650
+ viewportW = measurements[v].width;
2651
+ viewportH = measurements[v].height + measurements[v].y;
2652
+ break;
2152
2653
  }
2153
2654
  }
2154
2655
 
2155
- // Phase 2b: If searching by component and no match found,
2156
- // look for the component by name and walk UP to find nearest pressable ancestor
2157
- if (matches.length === 0 && searchComponent !== null) {
2158
- var componentCandidates = [];
2656
+ // Filter visible and match
2657
+ var matches = [];
2658
+ for (var i = 0; i < measurements.length; i++) {
2659
+ var m = measurements[i];
2660
+ if (!m) continue;
2661
+
2662
+ // Visibility filter: positive dimensions, within viewport
2663
+ if (m.width <= 0 || m.height <= 0) continue;
2664
+ if (m.x + m.width < 0 || m.y + m.height < 0) continue;
2665
+ if (m.x > viewportW || m.y > viewportH) continue;
2159
2666
 
2160
- function findByName(fiber, path) {
2161
- if (!fiber) return;
2162
- var name = getComponentName(fiber);
2163
- var props = fiber.memoizedProps;
2164
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
2667
+ var info = meta[i];
2165
2668
 
2166
- if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
2167
- componentCandidates.push({ fiber: fiber, name: name, path: path.join(' > ') });
2168
- }
2169
- var child = fiber.child;
2170
- while (child) {
2171
- var childName = getComponentName(child);
2172
- findByName(child, childName ? path.concat([childName]) : path);
2173
- child = child.sibling;
2174
- }
2669
+ // Match by query
2670
+ var matched = true;
2671
+ if (searchText !== null) {
2672
+ matched = matched && info.text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
2673
+ }
2674
+ if (searchTestID !== null) {
2675
+ matched = matched && (info.testID === searchTestID);
2676
+ }
2677
+ if (searchComponent !== null) {
2678
+ matched = matched && (info.name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
2175
2679
  }
2176
2680
 
2177
- for (var ri2 = 0; ri2 < roots.length; ri2++) {
2178
- findByName(roots[ri2].current, []);
2179
- }
2180
-
2181
- // For each candidate, walk up to find pressable ancestor
2182
- var maxDepth = ${maxTraversalDepth};
2183
- for (var ci = 0; ci < componentCandidates.length; ci++) {
2184
- var candidate = componentCandidates[ci];
2185
- var parent = candidate.fiber.return;
2186
- var depth = 0;
2187
- while (parent && depth < maxDepth) {
2188
- var parentProps = parent.memoizedProps;
2189
- if (parentProps && typeof parentProps.onPress === 'function') {
2190
- var text = extractText(parent, 0);
2191
- matches.push({
2192
- fiber: parent,
2193
- name: candidate.name,
2194
- text: text.substring(0, 100),
2195
- testID: parentProps.testID || parentProps.nativeID || null,
2196
- path: candidate.path
2197
- });
2198
- break;
2199
- }
2200
- parent = parent.return;
2201
- depth++;
2202
- }
2681
+ if (matched) {
2682
+ matches.push({
2683
+ name: info.name,
2684
+ text: info.text,
2685
+ testID: info.testID,
2686
+ path: info.path,
2687
+ isInput: info.isInput,
2688
+ x: Math.round(m.x + m.width / 2),
2689
+ y: Math.round(m.y + m.height / 2)
2690
+ });
2203
2691
  }
2204
2692
  }
2205
2693
 
@@ -2208,12 +2696,22 @@ export async function pressElement(options) {
2208
2696
  if (searchText !== null) criteria.push('text="' + searchText + '"');
2209
2697
  if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
2210
2698
  if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
2211
- return { error: 'No pressable or focusable elements found matching: ' + criteria.join(', ') };
2699
+ return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
2700
+ }
2701
+
2702
+ // When searching by text, prefer the most specific match.
2703
+ // A button labeled "Send code" is a better match than a
2704
+ // full-screen TouchableWithoutFeedback whose concatenated
2705
+ // child text happens to contain "Send code".
2706
+ if (searchText !== null && matches.length > 1) {
2707
+ matches.sort(function(a, b) {
2708
+ return a.text.length - b.text.length;
2709
+ });
2212
2710
  }
2213
2711
 
2214
2712
  if (targetIndex >= matches.length) {
2215
2713
  return {
2216
- error: 'Found ' + matches.length + ' match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
2714
+ error: 'Found ' + matches.length + ' visible match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
2217
2715
  matches: matches.map(function(m, i) {
2218
2716
  return { index: i, component: m.name, text: m.text, testID: m.testID };
2219
2717
  })
@@ -2221,137 +2719,28 @@ export async function pressElement(options) {
2221
2719
  }
2222
2720
 
2223
2721
  var target = matches[targetIndex];
2224
- try {
2225
- // Both pressable and input elements use native tap via measured coordinates.
2226
- // This ensures onPress wrappers, analytics, debouncing, and state tracking
2227
- // logic inside components is executed through React's event pipeline,
2228
- // rather than directly invoking the parent-supplied onPress prop.
2229
- var baseResult = {
2230
- success: true,
2231
- pressed: target.name,
2232
- matchIndex: targetIndex,
2233
- totalMatches: matches.length,
2234
- text: target.text,
2235
- testID: target.testID,
2236
- path: target.path,
2237
- isInput: !!target.isInput,
2238
- needsNativeTap: true,
2239
- nativeTapTarget: null
2240
- };
2241
- if (matches.length > 1) {
2242
- baseResult.allMatches = matches.map(function(m, i) {
2243
- return { index: i, component: m.name, text: m.text, testID: m.testID };
2244
- });
2245
- }
2246
-
2247
- // Walk down from the matched fiber to find a host component (tag === 5)
2248
- // whose screen position can be measured for a native tap.
2249
- // For inputs, prefer TextInput-specific hosts over generic RCTView.
2250
- var hostFiber = null;
2251
- var fallbackHost = null;
2252
- function findHostChild(fiber, depth) {
2253
- if (!fiber || depth > 5000 || hostFiber) return;
2254
- if (fiber.tag === 5 && fiber.stateNode) {
2255
- var hostType = typeof fiber.type === 'string' ? fiber.type : '';
2256
- if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
2257
- hostFiber = fiber;
2258
- return;
2259
- }
2260
- if (!fallbackHost) fallbackHost = fiber;
2261
- }
2262
- var child = fiber.child;
2263
- while (child) { findHostChild(child, depth + 1); child = child.sibling; }
2264
- }
2265
- if (target.fiber.tag === 5 && target.fiber.stateNode) {
2266
- hostFiber = target.fiber;
2267
- } else {
2268
- findHostChild(target.fiber, 0);
2269
- if (!hostFiber) hostFiber = fallbackHost;
2270
- }
2271
-
2272
- if (hostFiber && hostFiber.stateNode) {
2273
- var sn = hostFiber.stateNode;
2274
-
2275
- // Fabric (New Architecture): stateNode.node is the shadow node.
2276
- // nativeFabricUIManager.measure callback fires synchronously.
2277
- var fabricUI = globalThis.nativeFabricUIManager;
2278
- if (sn.node && fabricUI && typeof fabricUI.measure === 'function') {
2279
- try {
2280
- fabricUI.measure(sn.node, function(x, y, width, height, pageX, pageY) {
2281
- if (width > 0 && height > 0) {
2282
- baseResult.nativeTapTarget = {
2283
- x: Math.round(pageX + width / 2),
2284
- y: Math.round(pageY + height / 2),
2285
- unit: 'points'
2286
- };
2287
- }
2288
- });
2289
-
2290
- // Modal offset correction: if the element is inside a native modal
2291
- // (RNSModalScreen), measureInWindow returns coordinates relative to
2292
- // the modal's window, not the screen. Detect and add the offset.
2293
- if (baseResult.nativeTapTarget) {
2294
- var modalNode = null;
2295
- var screenStackNode = null;
2296
- var ancestor = target.fiber.return;
2297
- var walkCount = 0;
2298
- while (ancestor && walkCount < 200) {
2299
- var aType = typeof ancestor.type === 'string' ? ancestor.type : '';
2300
- if (ancestor.tag === 5 && ancestor.stateNode && ancestor.stateNode.node) {
2301
- if (aType === 'RNSModalScreen' && !modalNode) modalNode = ancestor.stateNode.node;
2302
- if (aType === 'RNSScreenStack') screenStackNode = ancestor.stateNode.node;
2303
- }
2304
- ancestor = ancestor.return;
2305
- walkCount++;
2306
- }
2307
- if (modalNode && screenStackNode) {
2308
- var modalH = 0, stackH = 0;
2309
- fabricUI.measureInWindow(modalNode, function(mx, my, mw, mh) { modalH = mh; });
2310
- fabricUI.measureInWindow(screenStackNode, function(sx, sy, sw, sh) { stackH = sh; });
2311
- if (stackH > modalH && modalH > 0) {
2312
- baseResult.nativeTapTarget.y += (stackH - modalH);
2313
- }
2314
- }
2315
- }
2316
- } catch (e) {
2317
- // Measurement failed — fall through without coordinates
2318
- }
2319
- }
2320
-
2321
- // Old Architecture (Bridge): stateNode._nativeTag
2322
- if (!baseResult.nativeTapTarget) {
2323
- var nativeTag = sn._nativeTag || sn.__nativeTag;
2324
- if (!nativeTag && typeof sn.getNativeTag === 'function') {
2325
- nativeTag = sn.getNativeTag();
2326
- }
2327
- if (nativeTag) {
2328
- var UIManager = globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.UIManager;
2329
- if (UIManager && typeof UIManager.measureInWindow === 'function') {
2330
- try {
2331
- UIManager.measureInWindow(nativeTag, function(x, y, width, height) {
2332
- if (width > 0 && height > 0) {
2333
- baseResult.nativeTapTarget = {
2334
- x: Math.round(x + width / 2),
2335
- y: Math.round(y + height / 2),
2336
- unit: 'points'
2337
- };
2338
- }
2339
- });
2340
- } catch (e) {
2341
- // Measurement failed
2342
- }
2343
- }
2344
- }
2345
- }
2346
- }
2347
-
2348
- return baseResult;
2349
- } catch (e) {
2350
- return { error: 'Handler threw: ' + (e.message || String(e)), component: target.name };
2722
+ var result = {
2723
+ success: true,
2724
+ pressed: target.name,
2725
+ matchIndex: targetIndex,
2726
+ totalMatches: matches.length,
2727
+ text: target.text,
2728
+ testID: target.testID,
2729
+ path: target.path,
2730
+ isInput: target.isInput,
2731
+ x: target.x,
2732
+ y: target.y,
2733
+ unit: 'points'
2734
+ };
2735
+ if (matches.length > 1) {
2736
+ result.allMatches = matches.map(function(m, i) {
2737
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
2738
+ });
2351
2739
  }
2740
+ return result;
2352
2741
  })()
2353
2742
  `;
2354
- return executeInApp(expression, false, {}, options.device);
2743
+ return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, device);
2355
2744
  }
2356
2745
  // ============================================================================
2357
2746
  // Coordinate-Based Element Inspection (via DevTools Inspector API)