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.
- package/README.md +1 -0
- package/build/__tests__/unit/tap.test.js +9 -9
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/executor.d.ts +24 -0
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +669 -280
- package/build/core/executor.js.map +1 -1
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +2 -0
- package/build/core/guides.js.map +1 -1
- package/build/core/index.d.ts +1 -1
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +1 -1
- package/build/core/index.js.map +1 -1
- package/build/index.js +31 -3
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +85 -134
- package/build/pro/tap.js.map +1 -1
- package/package.json +1 -1
package/build/core/executor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
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
|
|
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
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
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
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
|
2084
|
-
|
|
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
|
-
|
|
2092
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
-
|
|
2121
|
-
|
|
2606
|
+
})(mi);
|
|
2607
|
+
} catch(e) {}
|
|
2608
|
+
}
|
|
2122
2609
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
-
//
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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(
|
|
2743
|
+
return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, device);
|
|
2355
2744
|
}
|
|
2356
2745
|
// ============================================================================
|
|
2357
2746
|
// Coordinate-Based Element Inspection (via DevTools Inspector API)
|