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.
- package/README.md +1 -0
- package/build/__tests__/unit/tap.test.js +1 -1
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/appDetection.js +4 -5
- package/build/core/appDetection.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 +881 -389
- 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 +39 -4
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +39 -103
- package/build/pro/tap.js.map +1 -1
- package/package.json +1 -1
package/build/core/executor.js
CHANGED
|
@@ -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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1606
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1896
|
-
|
|
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
|
|
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
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
|
1971
|
-
|
|
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
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
-
|
|
2008
|
-
|
|
2606
|
+
})(mi);
|
|
2607
|
+
} catch(e) {}
|
|
2608
|
+
}
|
|
2009
2609
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
-
//
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
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;
|
|
2046
2661
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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(
|
|
2733
|
+
return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, device);
|
|
2242
2734
|
}
|
|
2243
2735
|
// ============================================================================
|
|
2244
2736
|
// Coordinate-Based Element Inspection (via DevTools Inspector API)
|