react-native-ai-devtools 1.4.26 → 1.4.28

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.
@@ -158,11 +158,21 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
158
158
  return { success: false, error: validation.error };
159
159
  }
160
160
  const cleanedExpression = validation.expression;
161
+ // Hermes CDP does not support awaitPromise — it serializes the Promise's
162
+ // internal fields (_A, _x, _y, _z) instead of waiting for resolution.
163
+ // When the caller wants awaitPromise, we handle it ourselves: wrap the
164
+ // expression to store the resolved value in a temp global, then poll.
165
+ if (awaitPromise) {
166
+ return executeWithManualAwait(app, cleanedExpression, timeoutMs);
167
+ }
168
+ return executeCDP(app, cleanedExpression, false, timeoutMs);
169
+ }
170
+ /**
171
+ * Execute a CDP Runtime.evaluate call (no promise awaiting).
172
+ */
173
+ function executeCDP(app, cleanedExpression, awaitPromise, timeoutMs) {
161
174
  const TIMEOUT_MS = timeoutMs;
162
175
  const currentMessageId = getNextMessageId();
163
- // Prepend global polyfill for Hermes compatibility
164
- // Runtime.evaluate returns the completion value of the last expression naturally,
165
- // so no IIFE wrapping is needed (similar to browser console behavior)
166
176
  const wrappedExpression = `${GLOBAL_POLYFILL} ${cleanedExpression}`;
167
177
  return new Promise((resolve) => {
168
178
  const timeoutId = setTimeout(() => {
@@ -217,6 +227,63 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
217
227
  }
218
228
  });
219
229
  }
230
+ /**
231
+ * Hermes workaround for awaitPromise: execute the expression, and if it
232
+ * returns a Promise, store the resolved/rejected value in a temp global
233
+ * and read it back with a small number of spaced-out retries.
234
+ */
235
+ async function executeWithManualAwait(app, cleanedExpression, timeoutMs) {
236
+ const slotId = `__rn_dbg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
237
+ // Wrap: run the expression, if result is thenable store resolved value in
238
+ // a temp global slot; otherwise store the sync value immediately.
239
+ const wrapperExpr = `(function(){
240
+ var __v=(${cleanedExpression});
241
+ if(__v&&typeof __v==='object'&&typeof __v.then==='function'){
242
+ globalThis['${slotId}']={s:'pending'};
243
+ __v.then(function(r){globalThis['${slotId}']={s:'ok',v:r}},function(e){globalThis['${slotId}']={s:'err',v:String(e)}});
244
+ return '__awaiting__'}
245
+ else{return __v}})()`;
246
+ const initial = await executeCDP(app, wrapperExpr, false, timeoutMs);
247
+ // If the expression didn't return a Promise, return the result directly
248
+ if (!initial.success || initial.result !== "__awaiting__") {
249
+ return initial;
250
+ }
251
+ // Read the settled value with a few spaced-out retries (not aggressive polling).
252
+ // Most Promises resolve within a microtask or a single event loop tick.
253
+ const RETRY_DELAYS_MS = [100, 300, 600, 1000, 2000, 3000];
254
+ const readExpr = `(function(){var s=globalThis['${slotId}'];if(!s||s.s==='pending')return '__pending__';delete globalThis['${slotId}'];return{status:s.s,value:s.v}})()`;
255
+ for (const delayMs of RETRY_DELAYS_MS) {
256
+ await new Promise((r) => setTimeout(r, delayMs));
257
+ const poll = await executeCDP(app, readExpr, false, 5000);
258
+ if (!poll.success)
259
+ return poll;
260
+ if (poll.result === "__pending__")
261
+ continue;
262
+ // The poll result comes through formatRemoteObject — objects are
263
+ // JSON.stringified, so we need to parse it back.
264
+ try {
265
+ const parsed = typeof poll.result === "string" ? JSON.parse(poll.result) : poll.result;
266
+ if (parsed?.status === "err") {
267
+ return { success: false, error: parsed.value || "Promise rejected" };
268
+ }
269
+ const value = parsed?.value;
270
+ return {
271
+ success: true,
272
+ result: value === undefined || value === null
273
+ ? String(value)
274
+ : typeof value === "object"
275
+ ? JSON.stringify(value, null, 2)
276
+ : String(value)
277
+ };
278
+ }
279
+ catch {
280
+ return poll;
281
+ }
282
+ }
283
+ // Cleanup on timeout
284
+ await executeCDP(app, `delete globalThis['${slotId}']`, false, 2000).catch(() => { });
285
+ return { success: false, error: "Timeout: Promise did not resolve within the time limit." };
286
+ }
220
287
  // Execute JavaScript in the connected React Native app with retry logic
221
288
  export async function executeInApp(expression, awaitPromise = true, options = {}, device) {
222
289
  const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
@@ -798,29 +865,34 @@ export async function getComponentTree(options = {}) {
798
865
  }
799
866
 
800
867
  function extractLayoutStyles(style) {
801
- if (!style) return null;
802
- const merged = Array.isArray(style)
803
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
804
- : (typeof style === 'object' ? style : {});
805
-
806
- const layout = {};
807
- const layoutKeys = [
808
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
809
- 'paddingHorizontal', 'paddingVertical',
810
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
811
- 'marginHorizontal', 'marginVertical',
812
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
813
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
814
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
815
- 'position', 'top', 'bottom', 'left', 'right',
816
- 'gap', 'rowGap', 'columnGap',
817
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
818
- ];
819
-
820
- for (const key of layoutKeys) {
821
- if (merged[key] !== undefined) layout[key] = merged[key];
822
- }
823
- return Object.keys(layout).length > 0 ? layout : null;
868
+ try {
869
+ if (!style) return null;
870
+ const merged = Array.isArray(style)
871
+ ? Object.assign({}, ...style.filter(Boolean).map(s => {
872
+ try { return typeof s === 'object' ? s : {}; }
873
+ catch { return {}; }
874
+ }))
875
+ : (typeof style === 'object' ? style : {});
876
+
877
+ const layout = {};
878
+ const layoutKeys = [
879
+ 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
880
+ 'paddingHorizontal', 'paddingVertical',
881
+ 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
882
+ 'marginHorizontal', 'marginVertical',
883
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
884
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
885
+ 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
886
+ 'position', 'top', 'bottom', 'left', 'right',
887
+ 'gap', 'rowGap', 'columnGap',
888
+ 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
889
+ ];
890
+
891
+ for (const key of layoutKeys) {
892
+ if (merged[key] !== undefined) layout[key] = merged[key];
893
+ }
894
+ return Object.keys(layout).length > 0 ? layout : null;
895
+ } catch { return null; }
824
896
  }
825
897
 
826
898
  function walkFiber(fiber, depth) {
@@ -849,23 +921,29 @@ export async function getComponentTree(options = {}) {
849
921
  const props = {};
850
922
  for (const key of Object.keys(fiber.memoizedProps)) {
851
923
  if (key === 'children' || key === 'style') continue;
852
- const val = fiber.memoizedProps[key];
853
- if (typeof val === 'function') {
854
- props[key] = '[Function]';
855
- } else if (typeof val === 'object' && val !== null) {
856
- props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
857
- } else {
858
- props[key] = val;
924
+ try {
925
+ const val = fiber.memoizedProps[key];
926
+ if (typeof val === 'function') {
927
+ props[key] = '[Function]';
928
+ } else if (typeof val === 'object' && val !== null) {
929
+ props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
930
+ } else {
931
+ props[key] = val;
932
+ }
933
+ } catch {
934
+ props[key] = '[Animated Value]';
859
935
  }
860
936
  }
861
937
  if (Object.keys(props).length > 0) node.props = props;
862
938
  }
863
939
 
864
940
  // Include layout styles if requested
865
- if (includeStyles && fiber.memoizedProps?.style) {
866
- const layout = extractLayoutStyles(fiber.memoizedProps.style);
867
- if (layout) node.layout = layout;
868
- }
941
+ try {
942
+ if (includeStyles && fiber.memoizedProps?.style) {
943
+ const layout = extractLayoutStyles(fiber.memoizedProps.style);
944
+ if (layout) node.layout = layout;
945
+ }
946
+ } catch { /* animated style — skip */ }
869
947
 
870
948
  // Traverse children
871
949
  let child = fiber.child;
@@ -1232,31 +1310,36 @@ export async function getScreenLayout(options = {}) {
1232
1310
  }
1233
1311
 
1234
1312
  function extractLayoutStyles(style) {
1235
- if (!style) return null;
1236
- var merged = Array.isArray(style)
1237
- ? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
1238
- : (typeof style === 'object' ? style : {});
1239
-
1240
- var layout = {};
1241
- var layoutKeys = [
1242
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1243
- 'paddingHorizontal', 'paddingVertical',
1244
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
1245
- 'marginHorizontal', 'marginVertical',
1246
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
1247
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
1248
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
1249
- 'position', 'top', 'bottom', 'left', 'right',
1250
- 'gap', 'rowGap', 'columnGap',
1251
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
1252
- 'backgroundColor', 'borderColor', 'borderRadius',
1253
- 'zIndex', 'elevation'
1254
- ];
1255
-
1256
- for (var k = 0; k < layoutKeys.length; k++) {
1257
- if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
1258
- }
1259
- return Object.keys(layout).length > 0 ? layout : null;
1313
+ try {
1314
+ if (!style) return null;
1315
+ var merged = Array.isArray(style)
1316
+ ? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) {
1317
+ try { return typeof s === 'object' ? s : {}; }
1318
+ catch(e) { return {}; }
1319
+ })))
1320
+ : (typeof style === 'object' ? style : {});
1321
+
1322
+ var layout = {};
1323
+ var layoutKeys = [
1324
+ 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1325
+ 'paddingHorizontal', 'paddingVertical',
1326
+ 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
1327
+ 'marginHorizontal', 'marginVertical',
1328
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
1329
+ 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
1330
+ 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
1331
+ 'position', 'top', 'bottom', 'left', 'right',
1332
+ 'gap', 'rowGap', 'columnGap',
1333
+ 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
1334
+ 'backgroundColor', 'borderColor', 'borderRadius',
1335
+ 'zIndex', 'elevation'
1336
+ ];
1337
+
1338
+ for (var k = 0; k < layoutKeys.length; k++) {
1339
+ if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
1340
+ }
1341
+ return Object.keys(layout).length > 0 ? layout : null;
1342
+ } catch(e) { return null; }
1260
1343
  }
1261
1344
 
1262
1345
  var elements = [];
@@ -1277,13 +1360,15 @@ export async function getScreenLayout(options = {}) {
1277
1360
  if (!displayName) continue;
1278
1361
  if (componentsOnly && !info.customName) continue;
1279
1362
 
1280
- var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
1363
+ var style = null;
1364
+ try { style = fiber.memoizedProps ? fiber.memoizedProps.style : null; } catch {}
1281
1365
  var layout = extractLayoutStyles(style);
1282
1366
 
1283
1367
  // Get text content — use pre-collected text from step 1, or fall back to host fiber
1284
1368
  var textContent = info.text || null;
1285
1369
  if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
1286
- var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
1370
+ var children = null;
1371
+ try { children = fiber.memoizedProps ? fiber.memoizedProps.children : null; } catch {}
1287
1372
  if (typeof children === 'string') textContent = children;
1288
1373
  else if (typeof children === 'number') textContent = String(children);
1289
1374
  }
@@ -1395,6 +1480,350 @@ export async function getScreenLayout(options = {}) {
1395
1480
  }
1396
1481
  return result;
1397
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
+ }
1398
1827
  /**
1399
1828
  * Format enriched elements as an indented tree with tap coordinates in pixels.
1400
1829
  * Same tree structure as get_screen_layout but with tap:(x,y) per node.
@@ -1506,11 +1935,16 @@ export async function inspectComponent(componentName, options = {}) {
1506
1935
  }
1507
1936
 
1508
1937
  function extractStyles(style) {
1509
- if (!style) return null;
1510
- const merged = Array.isArray(style)
1511
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1512
- : (typeof style === 'object' ? style : {});
1513
- return Object.keys(merged).length > 0 ? merged : null;
1938
+ try {
1939
+ if (!style) return null;
1940
+ const merged = Array.isArray(style)
1941
+ ? Object.assign({}, ...style.filter(Boolean).map(s => {
1942
+ try { return typeof s === 'object' ? s : {}; }
1943
+ catch { return {}; }
1944
+ }))
1945
+ : (typeof style === 'object' ? style : {});
1946
+ return Object.keys(merged).length > 0 ? merged : null;
1947
+ } catch { return { _note: '[Contains animated styles]' }; }
1514
1948
  }
1515
1949
 
1516
1950
  function serializeValue(val, depth = 0) {
@@ -1528,7 +1962,11 @@ export async function inspectComponent(componentName, options = {}) {
1528
1962
  if (keys.length > 20) return '[Object(' + keys.length + ' keys)]';
1529
1963
  const result = {};
1530
1964
  for (const k of keys) {
1531
- result[k] = serializeValue(val[k], depth + 1);
1965
+ try {
1966
+ result[k] = serializeValue(val[k], depth + 1);
1967
+ } catch {
1968
+ result[k] = '[Animated Value]';
1969
+ }
1532
1970
  }
1533
1971
  return result;
1534
1972
  }
@@ -1596,42 +2034,56 @@ export async function inspectComponent(componentName, options = {}) {
1596
2034
  const props = {};
1597
2035
  for (const key of Object.keys(fiber.memoizedProps)) {
1598
2036
  if (key === 'children') continue;
1599
- props[key] = serializeValue(fiber.memoizedProps[key]);
2037
+ try {
2038
+ props[key] = serializeValue(fiber.memoizedProps[key]);
2039
+ } catch {
2040
+ props[key] = '[Animated Value]';
2041
+ }
1600
2042
  }
1601
2043
  result.props = props;
1602
2044
  }
1603
2045
 
1604
2046
  // Style separately for clarity
1605
- if (fiber.memoizedProps?.style) {
1606
- result.style = extractStyles(fiber.memoizedProps.style);
2047
+ try {
2048
+ if (fiber.memoizedProps?.style) {
2049
+ result.style = extractStyles(fiber.memoizedProps.style);
2050
+ }
2051
+ } catch {
2052
+ result.style = { _note: '[Contains animated styles]' };
1607
2053
  }
1608
2054
 
1609
2055
  // State (for hooks, this is a linked list)
1610
2056
  if (includeState && fiber.memoizedState) {
1611
2057
  // Simplified hook value serialization
1612
2058
  function serializeHookValue(val, depth = 0) {
1613
- if (depth > 2) return '[...]';
1614
- if (val === null || val === undefined) return val;
1615
- if (typeof val === 'function') return '[Function]';
1616
- if (typeof val !== 'object') return val;
1617
- // Skip React internal structures (effects, refs with destroy/create)
1618
- if (val.create && val.destroy !== undefined) return '[Effect]';
1619
- if (val.inst && val.deps) return '[Effect]';
1620
- if (val.current !== undefined && Object.keys(val).length === 1) {
1621
- // Ref object - just show current value
1622
- return { current: serializeHookValue(val.current, depth + 1) };
1623
- }
1624
- if (Array.isArray(val)) {
1625
- if (val.length > 5) return '[Array(' + val.length + ')]';
1626
- return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
1627
- }
1628
- const keys = Object.keys(val);
1629
- if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
1630
- const result = {};
1631
- for (const k of keys.slice(0, 10)) {
1632
- result[k] = serializeHookValue(val[k], depth + 1);
1633
- }
1634
- return result;
2059
+ try {
2060
+ if (depth > 2) return '[...]';
2061
+ if (val === null || val === undefined) return val;
2062
+ if (typeof val === 'function') return '[Function]';
2063
+ if (typeof val !== 'object') return val;
2064
+ // Skip React internal structures (effects, refs with destroy/create)
2065
+ if (val.create && val.destroy !== undefined) return '[Effect]';
2066
+ if (val.inst && val.deps) return '[Effect]';
2067
+ if (val.current !== undefined && Object.keys(val).length === 1) {
2068
+ // Ref object - just show current value
2069
+ return { current: serializeHookValue(val.current, depth + 1) };
2070
+ }
2071
+ if (Array.isArray(val)) {
2072
+ if (val.length > 5) return '[Array(' + val.length + ')]';
2073
+ return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
2074
+ }
2075
+ const keys = Object.keys(val);
2076
+ if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
2077
+ const result = {};
2078
+ for (const k of keys.slice(0, 10)) {
2079
+ try {
2080
+ result[k] = serializeHookValue(val[k], depth + 1);
2081
+ } catch {
2082
+ result[k] = '[Animated Value]';
2083
+ }
2084
+ }
2085
+ return result;
2086
+ } catch { return '[Animated Value]'; }
1635
2087
  }
1636
2088
 
1637
2089
  // For function components with hooks
@@ -1717,20 +2169,25 @@ export async function findComponents(pattern, options = {}) {
1717
2169
  }
1718
2170
 
1719
2171
  function extractLayoutStyles(style) {
1720
- if (!style) return null;
1721
- const merged = Array.isArray(style)
1722
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1723
- : (typeof style === 'object' ? style : {});
1724
-
1725
- const layout = {};
1726
- const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1727
- 'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
1728
- 'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
1729
- 'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
1730
- for (const k of keys) {
1731
- if (merged[k] !== undefined) layout[k] = merged[k];
1732
- }
1733
- return Object.keys(layout).length > 0 ? layout : null;
2172
+ try {
2173
+ if (!style) return null;
2174
+ const merged = Array.isArray(style)
2175
+ ? Object.assign({}, ...style.filter(Boolean).map(s => {
2176
+ try { return typeof s === 'object' ? s : {}; }
2177
+ catch { return {}; }
2178
+ }))
2179
+ : (typeof style === 'object' ? style : {});
2180
+
2181
+ const layout = {};
2182
+ const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
2183
+ 'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
2184
+ 'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
2185
+ 'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
2186
+ for (const k of keys) {
2187
+ if (merged[k] !== undefined) layout[k] = merged[k];
2188
+ }
2189
+ return Object.keys(layout).length > 0 ? layout : null;
2190
+ } catch { return null; }
1734
2191
  }
1735
2192
 
1736
2193
  const results = [];
@@ -1831,7 +2288,7 @@ export async function findComponents(pattern, options = {}) {
1831
2288
  * Matches by text content, testID, or component name.
1832
2289
  */
1833
2290
  export async function pressElement(options) {
1834
- const { text, testID, component, index = 0, maxTraversalDepth = 15 } = options;
2291
+ const { text, testID, component, index = 0, maxTraversalDepth = 15, device } = options;
1835
2292
  if (!text && !testID && !component) {
1836
2293
  return { success: false, error: "At least one of text, testID, or component must be provided." };
1837
2294
  }
@@ -1839,19 +2296,20 @@ export async function pressElement(options) {
1839
2296
  const textParam = text ? `'${esc(text)}'` : "null";
1840
2297
  const testIDParam = testID ? `'${esc(testID)}'` : "null";
1841
2298
  const componentParam = component ? `'${esc(component)}'` : "null";
1842
- const expression = `
2299
+ // --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
2300
+ const dispatchExpression = `
1843
2301
  (function() {
1844
2302
  var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1845
2303
  if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
1846
2304
 
1847
2305
  var roots = [];
1848
2306
  if (hook.getFiberRoots) {
1849
- var renderers = hook.renderers;
1850
- if (renderers) {
1851
- for (var entry of renderers) {
1852
- var r = Array.from(hook.getFiberRoots(entry[0]) || []);
1853
- if (r.length > 0) { roots = r; break; }
1854
- }
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; }
1855
2313
  }
1856
2314
  }
1857
2315
  if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
@@ -1860,6 +2318,7 @@ export async function pressElement(options) {
1860
2318
  var searchTestID = ${testIDParam};
1861
2319
  var searchComponent = ${componentParam};
1862
2320
  var targetIndex = ${index};
2321
+ var maxTraversalUp = ${maxTraversalDepth};
1863
2322
 
1864
2323
  function getComponentName(fiber) {
1865
2324
  if (!fiber || !fiber.type) return null;
@@ -1890,15 +2349,63 @@ export async function pressElement(options) {
1890
2349
  return parts.join('');
1891
2350
  }
1892
2351
 
1893
- 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
+ }
1894
2362
 
1895
- function walkFiber(fiber, path) {
1896
- 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;
1897
2402
  var name = getComponentName(fiber);
1898
2403
  var props = fiber.memoizedProps;
1899
2404
 
1900
- // Skip off-screen subtrees — React Navigation marks hidden screens with aria-hidden
2405
+ // Skip inactive/hidden screens
1901
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;
1902
2409
 
1903
2410
  var isPressable = props && typeof props.onPress === 'function';
1904
2411
  var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
@@ -1908,33 +2415,24 @@ export async function pressElement(options) {
1908
2415
  if (isPressable) {
1909
2416
  text = extractText(fiber, 0);
1910
2417
  } else {
1911
- // For inputs: use child text, value, defaultValue, or placeholder
1912
2418
  var val = typeof props.value === 'string' ? props.value : '';
1913
2419
  var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
1914
2420
  var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
1915
2421
  text = extractText(fiber, 0) || val || defVal || ph;
1916
2422
  }
1917
2423
  var tid = props.testID || props.nativeID || null;
1918
- var matched = true;
1919
2424
 
1920
- if (searchText !== null) {
1921
- matched = matched && text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
1922
- }
1923
- if (searchTestID !== null) {
1924
- matched = matched && (tid === searchTestID);
1925
- }
1926
- if (searchComponent !== null) {
1927
- matched = matched && (name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
1928
- }
1929
-
1930
- if (matched) {
1931
- matches.push({
1932
- fiber: fiber,
2425
+ var host = findFirstHost(fiber, 0, isInput);
2426
+ if (host) {
2427
+ hostFibers.push(host);
2428
+ tapMeta.push({
1933
2429
  name: name || '(anonymous)',
1934
2430
  text: text.substring(0, 100),
1935
2431
  testID: tid,
1936
2432
  path: path.join(' > '),
1937
- isInput: isInput
2433
+ isInput: isInput,
2434
+ isPressable: isPressable,
2435
+ source: 'direct'
1938
2436
  });
1939
2437
  }
1940
2438
  }
@@ -1942,151 +2440,254 @@ export async function pressElement(options) {
1942
2440
  var child = fiber.child;
1943
2441
  while (child) {
1944
2442
  var childName = getComponentName(child);
1945
- walkFiber(child, childName ? path.concat([childName]) : path);
2443
+ walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
1946
2444
  child = child.sibling;
1947
2445
  }
1948
2446
  }
1949
2447
 
1950
2448
  for (var ri = 0; ri < roots.length; ri++) {
1951
- 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
+ }
1952
2528
  }
1953
2529
 
1954
- // Phase 2a: If searching by testID and no match found,
1955
- // the testID may be on a non-pressable/non-input wrapper — search ALL nodes
1956
- // for the testID and walk UP to find nearest pressable or input ancestor
1957
- if (matches.length === 0 && searchTestID !== null) {
1958
- var testIDCandidates = [];
1959
-
1960
- function findByTestID(fiber, path) {
1961
- if (!fiber) return;
1962
- var name = getComponentName(fiber);
1963
- var props = fiber.memoizedProps;
1964
- 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
+ }
1965
2538
 
1966
- var tid = props && (props.testID || props.nativeID || null);
1967
- if (tid === searchTestID) {
1968
- 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
+ }
1969
2580
  }
1970
- var child = fiber.child;
1971
- while (child) {
1972
- var childName = getComponentName(child);
1973
- findByTestID(child, childName ? path.concat([childName]) : path);
1974
- child = child.sibling;
2581
+ for (var ri2b = 0; ri2b < roots.length; ri2b++) {
2582
+ findByName2b(roots[ri2b].current, []);
1975
2583
  }
1976
2584
  }
2585
+ }
1977
2586
 
1978
- for (var ri3 = 0; ri3 < roots.length; ri3++) {
1979
- findByTestID(roots[ri3].current, []);
1980
- }
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
+ }
1981
2594
 
1982
- // For each candidate, first check if the node itself is pressable/input,
1983
- // then walk UP to find nearest pressable/input ancestor
1984
- var maxDepthTID = ${maxTraversalDepth};
1985
- for (var ti = 0; ti < testIDCandidates.length; ti++) {
1986
- var tCandidate = testIDCandidates[ti];
1987
- var tProps = tCandidate.fiber.memoizedProps;
1988
- var tIsPressable = tProps && typeof tProps.onPress === 'function';
1989
- 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);
1990
2599
 
1991
- if (tIsPressable || tIsInput) {
1992
- var text = extractText(tCandidate.fiber, 0);
1993
- if (tIsInput) {
1994
- var val = typeof tProps.value === 'string' ? tProps.value : '';
1995
- var defVal = typeof tProps.defaultValue === 'string' ? tProps.defaultValue : '';
1996
- var ph = typeof tProps.placeholder === 'string' ? tProps.placeholder : '';
1997
- text = text || val || defVal || ph;
1998
- }
1999
- matches.push({
2000
- fiber: tCandidate.fiber,
2001
- name: tCandidate.name,
2002
- text: text.substring(0, 100),
2003
- testID: searchTestID,
2004
- path: tCandidate.path,
2005
- 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 };
2006
2605
  });
2007
- continue;
2008
- }
2606
+ })(mi);
2607
+ } catch(e) {}
2608
+ }
2009
2609
 
2010
- // Walk up to find pressable/input ancestor
2011
- var tParent = tCandidate.fiber.return;
2012
- var tDepth = 0;
2013
- while (tParent && tDepth < maxDepthTID) {
2014
- var tParentProps = tParent.memoizedProps;
2015
- var parentIsPressable = tParentProps && typeof tParentProps.onPress === 'function';
2016
- var parentIsInput = !parentIsPressable && tParentProps && (typeof tParentProps.onChangeText === 'function' || typeof tParentProps.onFocus === 'function');
2017
-
2018
- if (parentIsPressable || parentIsInput) {
2019
- var text = extractText(tParent, 0);
2020
- if (parentIsInput) {
2021
- var val = typeof tParentProps.value === 'string' ? tParentProps.value : '';
2022
- var defVal = typeof tParentProps.defaultValue === 'string' ? tParentProps.defaultValue : '';
2023
- var ph = typeof tParentProps.placeholder === 'string' ? tParentProps.placeholder : '';
2024
- text = text || val || defVal || ph;
2025
- }
2026
- matches.push({
2027
- fiber: tParent,
2028
- name: tCandidate.name,
2029
- text: text.substring(0, 100),
2030
- testID: tParentProps.testID || tParentProps.nativeID || searchTestID,
2031
- path: tCandidate.path,
2032
- isInput: parentIsInput
2033
- });
2034
- break;
2035
- }
2036
- tParent = tParent.return;
2037
- tDepth++;
2038
- }
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;
2039
2653
  }
2040
2654
  }
2041
2655
 
2042
- // Phase 2b: If searching by component and no match found,
2043
- // look for the component by name and walk UP to find nearest pressable ancestor
2044
- if (matches.length === 0 && searchComponent !== null) {
2045
- 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;
2046
2661
 
2047
- function findByName(fiber, path) {
2048
- if (!fiber) return;
2049
- var name = getComponentName(fiber);
2050
- var props = fiber.memoizedProps;
2051
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
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;
2052
2666
 
2053
- if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
2054
- componentCandidates.push({ fiber: fiber, name: name, path: path.join(' > ') });
2055
- }
2056
- var child = fiber.child;
2057
- while (child) {
2058
- var childName = getComponentName(child);
2059
- findByName(child, childName ? path.concat([childName]) : path);
2060
- child = child.sibling;
2061
- }
2667
+ var info = meta[i];
2668
+
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;
2062
2679
  }
2063
2680
 
2064
- for (var ri2 = 0; ri2 < roots.length; ri2++) {
2065
- findByName(roots[ri2].current, []);
2066
- }
2067
-
2068
- // For each candidate, walk up to find pressable ancestor
2069
- var maxDepth = ${maxTraversalDepth};
2070
- for (var ci = 0; ci < componentCandidates.length; ci++) {
2071
- var candidate = componentCandidates[ci];
2072
- var parent = candidate.fiber.return;
2073
- var depth = 0;
2074
- while (parent && depth < maxDepth) {
2075
- var parentProps = parent.memoizedProps;
2076
- if (parentProps && typeof parentProps.onPress === 'function') {
2077
- var text = extractText(parent, 0);
2078
- matches.push({
2079
- fiber: parent,
2080
- name: candidate.name,
2081
- text: text.substring(0, 100),
2082
- testID: parentProps.testID || parentProps.nativeID || null,
2083
- path: candidate.path
2084
- });
2085
- break;
2086
- }
2087
- parent = parent.return;
2088
- depth++;
2089
- }
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
+ });
2090
2691
  }
2091
2692
  }
2092
2693
 
@@ -2095,12 +2696,12 @@ export async function pressElement(options) {
2095
2696
  if (searchText !== null) criteria.push('text="' + searchText + '"');
2096
2697
  if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
2097
2698
  if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
2098
- return { error: 'No pressable or focusable elements found matching: ' + criteria.join(', ') };
2699
+ return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
2099
2700
  }
2100
2701
 
2101
2702
  if (targetIndex >= matches.length) {
2102
2703
  return {
2103
- error: 'Found ' + matches.length + ' match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
2704
+ error: 'Found ' + matches.length + ' visible match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
2104
2705
  matches: matches.map(function(m, i) {
2105
2706
  return { index: i, component: m.name, text: m.text, testID: m.testID };
2106
2707
  })
@@ -2108,137 +2709,28 @@ export async function pressElement(options) {
2108
2709
  }
2109
2710
 
2110
2711
  var target = matches[targetIndex];
2111
- try {
2112
- // Both pressable and input elements use native tap via measured coordinates.
2113
- // This ensures onPress wrappers, analytics, debouncing, and state tracking
2114
- // logic inside components is executed through React's event pipeline,
2115
- // rather than directly invoking the parent-supplied onPress prop.
2116
- var baseResult = {
2117
- success: true,
2118
- pressed: target.name,
2119
- matchIndex: targetIndex,
2120
- totalMatches: matches.length,
2121
- text: target.text,
2122
- testID: target.testID,
2123
- path: target.path,
2124
- isInput: !!target.isInput,
2125
- needsNativeTap: true,
2126
- nativeTapTarget: null
2127
- };
2128
- if (matches.length > 1) {
2129
- baseResult.allMatches = matches.map(function(m, i) {
2130
- return { index: i, component: m.name, text: m.text, testID: m.testID };
2131
- });
2132
- }
2133
-
2134
- // Walk down from the matched fiber to find a host component (tag === 5)
2135
- // whose screen position can be measured for a native tap.
2136
- // For inputs, prefer TextInput-specific hosts over generic RCTView.
2137
- var hostFiber = null;
2138
- var fallbackHost = null;
2139
- function findHostChild(fiber, depth) {
2140
- if (!fiber || depth > 5000 || hostFiber) return;
2141
- if (fiber.tag === 5 && fiber.stateNode) {
2142
- var hostType = typeof fiber.type === 'string' ? fiber.type : '';
2143
- if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
2144
- hostFiber = fiber;
2145
- return;
2146
- }
2147
- if (!fallbackHost) fallbackHost = fiber;
2148
- }
2149
- var child = fiber.child;
2150
- while (child) { findHostChild(child, depth + 1); child = child.sibling; }
2151
- }
2152
- if (target.fiber.tag === 5 && target.fiber.stateNode) {
2153
- hostFiber = target.fiber;
2154
- } else {
2155
- findHostChild(target.fiber, 0);
2156
- if (!hostFiber) hostFiber = fallbackHost;
2157
- }
2158
-
2159
- if (hostFiber && hostFiber.stateNode) {
2160
- var sn = hostFiber.stateNode;
2161
-
2162
- // Fabric (New Architecture): stateNode.node is the shadow node.
2163
- // nativeFabricUIManager.measure callback fires synchronously.
2164
- var fabricUI = globalThis.nativeFabricUIManager;
2165
- if (sn.node && fabricUI && typeof fabricUI.measure === 'function') {
2166
- try {
2167
- fabricUI.measure(sn.node, function(x, y, width, height, pageX, pageY) {
2168
- if (width > 0 && height > 0) {
2169
- baseResult.nativeTapTarget = {
2170
- x: Math.round(pageX + width / 2),
2171
- y: Math.round(pageY + height / 2),
2172
- unit: 'points'
2173
- };
2174
- }
2175
- });
2176
-
2177
- // Modal offset correction: if the element is inside a native modal
2178
- // (RNSModalScreen), measureInWindow returns coordinates relative to
2179
- // the modal's window, not the screen. Detect and add the offset.
2180
- if (baseResult.nativeTapTarget) {
2181
- var modalNode = null;
2182
- var screenStackNode = null;
2183
- var ancestor = target.fiber.return;
2184
- var walkCount = 0;
2185
- while (ancestor && walkCount < 200) {
2186
- var aType = typeof ancestor.type === 'string' ? ancestor.type : '';
2187
- if (ancestor.tag === 5 && ancestor.stateNode && ancestor.stateNode.node) {
2188
- if (aType === 'RNSModalScreen' && !modalNode) modalNode = ancestor.stateNode.node;
2189
- if (aType === 'RNSScreenStack') screenStackNode = ancestor.stateNode.node;
2190
- }
2191
- ancestor = ancestor.return;
2192
- walkCount++;
2193
- }
2194
- if (modalNode && screenStackNode) {
2195
- var modalH = 0, stackH = 0;
2196
- fabricUI.measureInWindow(modalNode, function(mx, my, mw, mh) { modalH = mh; });
2197
- fabricUI.measureInWindow(screenStackNode, function(sx, sy, sw, sh) { stackH = sh; });
2198
- if (stackH > modalH && modalH > 0) {
2199
- baseResult.nativeTapTarget.y += (stackH - modalH);
2200
- }
2201
- }
2202
- }
2203
- } catch (e) {
2204
- // Measurement failed — fall through without coordinates
2205
- }
2206
- }
2207
-
2208
- // Old Architecture (Bridge): stateNode._nativeTag
2209
- if (!baseResult.nativeTapTarget) {
2210
- var nativeTag = sn._nativeTag || sn.__nativeTag;
2211
- if (!nativeTag && typeof sn.getNativeTag === 'function') {
2212
- nativeTag = sn.getNativeTag();
2213
- }
2214
- if (nativeTag) {
2215
- var UIManager = globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.UIManager;
2216
- if (UIManager && typeof UIManager.measureInWindow === 'function') {
2217
- try {
2218
- UIManager.measureInWindow(nativeTag, function(x, y, width, height) {
2219
- if (width > 0 && height > 0) {
2220
- baseResult.nativeTapTarget = {
2221
- x: Math.round(x + width / 2),
2222
- y: Math.round(y + height / 2),
2223
- unit: 'points'
2224
- };
2225
- }
2226
- });
2227
- } catch (e) {
2228
- // Measurement failed
2229
- }
2230
- }
2231
- }
2232
- }
2233
- }
2234
-
2235
- return baseResult;
2236
- } catch (e) {
2237
- return { error: 'Handler threw: ' + (e.message || String(e)), component: target.name };
2712
+ var result = {
2713
+ success: true,
2714
+ pressed: target.name,
2715
+ matchIndex: targetIndex,
2716
+ totalMatches: matches.length,
2717
+ text: target.text,
2718
+ testID: target.testID,
2719
+ path: target.path,
2720
+ isInput: target.isInput,
2721
+ x: target.x,
2722
+ y: target.y,
2723
+ unit: 'points'
2724
+ };
2725
+ if (matches.length > 1) {
2726
+ result.allMatches = matches.map(function(m, i) {
2727
+ return { index: i, component: m.name, text: m.text, testID: m.testID };
2728
+ });
2238
2729
  }
2730
+ return result;
2239
2731
  })()
2240
2732
  `;
2241
- return executeInApp(expression, false, {}, options.device);
2733
+ return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, device);
2242
2734
  }
2243
2735
  // ============================================================================
2244
2736
  // Coordinate-Based Element Inspection (via DevTools Inspector API)