react-native-ai-debugger 1.4.21 → 1.4.34
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 +95 -82
- package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
- package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
- package/build/__tests__/unit/fiberMatch.test.js +114 -0
- package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.js +113 -1
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +54 -98
- package/build/core/android.js.map +1 -1
- package/build/core/appDetection.d.ts.map +1 -1
- package/build/core/appDetection.js +29 -5
- package/build/core/appDetection.js.map +1 -1
- package/build/core/executor.d.ts +25 -0
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +1108 -385
- package/build/core/executor.js.map +1 -1
- package/build/core/feedback.d.ts +18 -0
- package/build/core/feedback.d.ts.map +1 -0
- package/build/core/feedback.js +73 -0
- package/build/core/feedback.js.map +1 -0
- package/build/core/fiberMatch.d.ts +25 -0
- package/build/core/fiberMatch.d.ts.map +1 -0
- package/build/core/fiberMatch.js +35 -0
- package/build/core/fiberMatch.js.map +1 -0
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +39 -0
- package/build/core/guides.js.map +1 -1
- package/build/core/index.d.ts +3 -2
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +4 -2
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +10 -5
- package/build/core/ios.js.map +1 -1
- package/build/core/license.d.ts.map +1 -1
- package/build/core/license.js +26 -35
- package/build/core/license.js.map +1 -1
- package/build/core/logbox.d.ts +6 -0
- package/build/core/logbox.d.ts.map +1 -1
- package/build/core/logbox.js +24 -0
- package/build/core/logbox.js.map +1 -1
- package/build/core/telemetry.d.ts +3 -0
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +18 -4
- package/build/core/telemetry.js.map +1 -1
- package/build/index.js +221 -54
- package/build/index.js.map +1 -1
- package/build/pro/tap.d.ts +13 -0
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +47 -14
- package/build/pro/tap.js.map +1 -1
- package/build/tools/accountTools.d.ts.map +1 -1
- package/build/tools/accountTools.js +3 -1
- package/build/tools/accountTools.js.map +1 -1
- package/package.json +1 -2
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js.map +0 -1
- package/build/core/httpServerProcess.d.ts +0 -25
- package/build/core/httpServerProcess.d.ts.map +0 -1
- package/build/core/httpServerProcess.js.map +0 -1
- package/build/core/tap.d.ts +0 -91
- package/build/core/tap.d.ts.map +0 -1
- package/build/core/tap.js +0 -542
- package/build/core/tap.js.map +0 -1
- package/build/httpServerStandalone.d.ts +0 -7
- package/build/httpServerStandalone.d.ts.map +0 -1
- package/build/httpServerStandalone.js.map +0 -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,436 @@ 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 findHostsInSubtree(fiber, depth, hosts, limit) {
|
|
1527
|
+
if (!fiber || depth > 20 || hosts.length >= limit) return;
|
|
1528
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
|
|
1529
|
+
hosts.push(fiber);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
var child = fiber.child;
|
|
1533
|
+
while (child && hosts.length < limit) {
|
|
1534
|
+
findHostsInSubtree(child, depth + 1, hosts, limit);
|
|
1535
|
+
child = child.sibling;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function collectText(fiber, d, isRoot) {
|
|
1540
|
+
if (!fiber || d > 30) return '';
|
|
1541
|
+
var props = fiber.memoizedProps;
|
|
1542
|
+
// Stop descent at nested pressable/input boundaries — their text belongs to them, not to the outer wrapper.
|
|
1543
|
+
if (!isRoot && props && (typeof props.onPress === 'function' ||
|
|
1544
|
+
typeof props.onChangeText === 'function' ||
|
|
1545
|
+
typeof props.onFocus === 'function')) {
|
|
1546
|
+
return '';
|
|
1547
|
+
}
|
|
1548
|
+
if (props) {
|
|
1549
|
+
var ch = props.children;
|
|
1550
|
+
if (typeof ch === 'string') return ch;
|
|
1551
|
+
if (typeof ch === 'number') return String(ch);
|
|
1552
|
+
if (Array.isArray(ch)) {
|
|
1553
|
+
var inline = [];
|
|
1554
|
+
for (var ci = 0; ci < ch.length; ci++) {
|
|
1555
|
+
if (typeof ch[ci] === 'string') inline.push(ch[ci]);
|
|
1556
|
+
else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
|
|
1557
|
+
}
|
|
1558
|
+
if (inline.length > 0) return inline.join('');
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
var parts = [];
|
|
1562
|
+
var child = fiber.child;
|
|
1563
|
+
while (child) {
|
|
1564
|
+
var t = collectText(child, d + 1, false);
|
|
1565
|
+
if (t) parts.push(t);
|
|
1566
|
+
child = child.sibling;
|
|
1567
|
+
}
|
|
1568
|
+
return parts.join(' ').trim();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function getComponentName(fiber) {
|
|
1572
|
+
if (!fiber || !fiber.type) return null;
|
|
1573
|
+
if (typeof fiber.type === 'string') return fiber.type;
|
|
1574
|
+
return fiber.type.displayName || fiber.type.name || null;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
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)$/;
|
|
1578
|
+
|
|
1579
|
+
var hostFibers = [];
|
|
1580
|
+
var fiberMeta = [];
|
|
1581
|
+
|
|
1582
|
+
function findMeaningfulAncestorName(fiber) {
|
|
1583
|
+
var cur = fiber.return;
|
|
1584
|
+
var depth = 0;
|
|
1585
|
+
var fallbackName = null;
|
|
1586
|
+
while (cur && depth < 20) {
|
|
1587
|
+
var name = getComponentName(cur);
|
|
1588
|
+
if (name && typeof cur.type !== 'string') {
|
|
1589
|
+
if (!fallbackName) fallbackName = name;
|
|
1590
|
+
if (!RN_PRIMITIVES.test(name)) return name;
|
|
1591
|
+
}
|
|
1592
|
+
cur = cur.return;
|
|
1593
|
+
depth++;
|
|
1594
|
+
}
|
|
1595
|
+
return fallbackName;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function buildPath(fiber) {
|
|
1599
|
+
var parts = [];
|
|
1600
|
+
var cur = fiber;
|
|
1601
|
+
var depth = 0;
|
|
1602
|
+
while (cur && depth < 30) {
|
|
1603
|
+
var name = getComponentName(cur);
|
|
1604
|
+
if (name && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(name)) {
|
|
1605
|
+
parts.unshift(name);
|
|
1606
|
+
}
|
|
1607
|
+
cur = cur.return;
|
|
1608
|
+
depth++;
|
|
1609
|
+
}
|
|
1610
|
+
// Keep last 3 segments
|
|
1611
|
+
if (parts.length > 3) {
|
|
1612
|
+
parts = parts.slice(-3);
|
|
1613
|
+
return '... > ' + parts.join(' > ');
|
|
1614
|
+
}
|
|
1615
|
+
return parts.join(' > ');
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function walkPressables(fiber, depth) {
|
|
1619
|
+
if (!fiber || depth > 5000) return;
|
|
1620
|
+
var name = getComponentName(fiber);
|
|
1621
|
+
var props = fiber.memoizedProps;
|
|
1622
|
+
|
|
1623
|
+
// Skip inactive/unfocused screens
|
|
1624
|
+
if (name === 'MaybeScreen' && props && props.active === 0) return;
|
|
1625
|
+
if (name === 'SceneView' && props && props.focused === false) return;
|
|
1626
|
+
if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
|
|
1627
|
+
|
|
1628
|
+
var isPressable = props && typeof props.onPress === 'function';
|
|
1629
|
+
var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
|
|
1630
|
+
|
|
1631
|
+
if (isPressable || isInput) {
|
|
1632
|
+
var hostsForThis = [];
|
|
1633
|
+
findHostsInSubtree(fiber, 0, hostsForThis, 16);
|
|
1634
|
+
if (hostsForThis.length > 0) {
|
|
1635
|
+
var text = collectText(fiber, 0, true);
|
|
1636
|
+
var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
|
|
1637
|
+
var path = buildPath(fiber);
|
|
1638
|
+
var testID = (props && (props.testID || props.nativeID)) || null;
|
|
1639
|
+
var accessibilityLabel = (props && props.accessibilityLabel) || null;
|
|
1640
|
+
|
|
1641
|
+
var hostIndices = [];
|
|
1642
|
+
for (var hi = 0; hi < hostsForThis.length; hi++) {
|
|
1643
|
+
hostIndices.push(hostFibers.length);
|
|
1644
|
+
hostFibers.push(hostsForThis[hi]);
|
|
1645
|
+
}
|
|
1646
|
+
fiberMeta.push({
|
|
1647
|
+
component: componentName,
|
|
1648
|
+
path: path,
|
|
1649
|
+
text: text ? text.slice(0, 100) : '',
|
|
1650
|
+
testID: testID,
|
|
1651
|
+
accessibilityLabel: accessibilityLabel,
|
|
1652
|
+
isInput: !!isInput,
|
|
1653
|
+
hostIndices: hostIndices
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
var child = fiber.child;
|
|
1659
|
+
while (child) {
|
|
1660
|
+
walkPressables(child, depth + 1);
|
|
1661
|
+
child = child.sibling;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
walkPressables(roots[0].current, 0);
|
|
1666
|
+
|
|
1667
|
+
if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
|
|
1668
|
+
|
|
1669
|
+
// Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
|
|
1670
|
+
var rootHost = findFirstHost(roots[0].current, 0);
|
|
1671
|
+
var rootIdx = -1;
|
|
1672
|
+
if (rootHost) {
|
|
1673
|
+
rootIdx = hostFibers.length;
|
|
1674
|
+
hostFibers.push(rootHost);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
globalThis.__pressableFibers = hostFibers;
|
|
1678
|
+
globalThis.__pressableMeta = fiberMeta;
|
|
1679
|
+
globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
|
|
1680
|
+
globalThis.__pressableRootIdx = rootIdx;
|
|
1681
|
+
|
|
1682
|
+
for (var i = 0; i < hostFibers.length; i++) {
|
|
1683
|
+
try {
|
|
1684
|
+
(function(idx) {
|
|
1685
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
1686
|
+
globalThis.__pressableMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
1687
|
+
});
|
|
1688
|
+
})(i);
|
|
1689
|
+
} catch(e) {}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
return { count: hostFibers.length };
|
|
1693
|
+
})()
|
|
1694
|
+
`;
|
|
1695
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
|
|
1696
|
+
if (!dispatchResult.success)
|
|
1697
|
+
return dispatchResult;
|
|
1698
|
+
try {
|
|
1699
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
1700
|
+
if (parsed.error)
|
|
1701
|
+
return { success: false, error: parsed.error };
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
/* ignore */
|
|
1705
|
+
}
|
|
1706
|
+
// Wait for measureInWindow callbacks
|
|
1707
|
+
await delay(300);
|
|
1708
|
+
// --- Step 2: read measurements, filter visible, build results ---
|
|
1709
|
+
const resolveExpression = `
|
|
1710
|
+
(function() {
|
|
1711
|
+
var fibers = globalThis.__pressableFibers;
|
|
1712
|
+
var meta = globalThis.__pressableMeta;
|
|
1713
|
+
var measurements = globalThis.__pressableMeasurements;
|
|
1714
|
+
var rootIdx = globalThis.__pressableRootIdx;
|
|
1715
|
+
globalThis.__pressableFibers = null;
|
|
1716
|
+
globalThis.__pressableMeta = null;
|
|
1717
|
+
globalThis.__pressableMeasurements = null;
|
|
1718
|
+
globalThis.__pressableRootIdx = null;
|
|
1719
|
+
|
|
1720
|
+
if (!fibers || !measurements || !meta) {
|
|
1721
|
+
return { error: 'No measurement data. Run get_pressable_elements again.' };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Get viewport dimensions from the explicit root measurement (fallback to scanning)
|
|
1725
|
+
var viewportW = 9999, viewportH = 9999;
|
|
1726
|
+
var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
|
|
1727
|
+
if (rootM && rootM.width > 0 && rootM.height > 0) {
|
|
1728
|
+
viewportW = rootM.width;
|
|
1729
|
+
viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
|
|
1730
|
+
} else {
|
|
1731
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
1732
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
|
|
1733
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
1734
|
+
viewportW = measurements[v].width;
|
|
1735
|
+
viewportH = measurements[v].height + measurements[v].y;
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
var elements = [];
|
|
1742
|
+
|
|
1743
|
+
for (var i = 0; i < meta.length; i++) {
|
|
1744
|
+
var info = meta[i];
|
|
1745
|
+
|
|
1746
|
+
// Union all host measurements for this pressable to get its true bounds
|
|
1747
|
+
var uMinX = Infinity, uMinY = Infinity, uMaxX = -Infinity, uMaxY = -Infinity;
|
|
1748
|
+
var hasValid = false;
|
|
1749
|
+
var indices = info.hostIndices || [];
|
|
1750
|
+
for (var hi2 = 0; hi2 < indices.length; hi2++) {
|
|
1751
|
+
var mm = measurements[indices[hi2]];
|
|
1752
|
+
if (!mm || mm.width <= 0 || mm.height <= 0) continue;
|
|
1753
|
+
hasValid = true;
|
|
1754
|
+
if (mm.x < uMinX) uMinX = mm.x;
|
|
1755
|
+
if (mm.y < uMinY) uMinY = mm.y;
|
|
1756
|
+
if (mm.x + mm.width > uMaxX) uMaxX = mm.x + mm.width;
|
|
1757
|
+
if (mm.y + mm.height > uMaxY) uMaxY = mm.y + mm.height;
|
|
1758
|
+
}
|
|
1759
|
+
if (!hasValid) continue;
|
|
1760
|
+
var m = { x: uMinX, y: uMinY, width: uMaxX - uMinX, height: uMaxY - uMinY };
|
|
1761
|
+
|
|
1762
|
+
// Filter: only visible within viewport
|
|
1763
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
1764
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
1765
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
1766
|
+
|
|
1767
|
+
var text = info.text || '';
|
|
1768
|
+
|
|
1769
|
+
elements.push({
|
|
1770
|
+
component: info.component,
|
|
1771
|
+
path: info.path,
|
|
1772
|
+
center: {
|
|
1773
|
+
x: Math.round(m.x + m.width / 2),
|
|
1774
|
+
y: Math.round(m.y + m.height / 2)
|
|
1775
|
+
},
|
|
1776
|
+
frame: {
|
|
1777
|
+
x: Math.round(m.x),
|
|
1778
|
+
y: Math.round(m.y),
|
|
1779
|
+
width: Math.round(m.width),
|
|
1780
|
+
height: Math.round(m.height)
|
|
1781
|
+
},
|
|
1782
|
+
text: text,
|
|
1783
|
+
testID: info.testID,
|
|
1784
|
+
accessibilityLabel: info.accessibilityLabel,
|
|
1785
|
+
hasLabel: text.length > 0,
|
|
1786
|
+
isInput: info.isInput
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
|
|
1791
|
+
// often share the same frame. Keep the one with the most meaningful component name,
|
|
1792
|
+
// but merge text/testID/accessibilityLabel from the loser so stopping collectText
|
|
1793
|
+
// at nested-pressable boundaries does not drop labels across the merge.
|
|
1794
|
+
var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
|
|
1795
|
+
function mergeFields(winner, loser) {
|
|
1796
|
+
if (!winner.text && loser.text) {
|
|
1797
|
+
winner.text = loser.text;
|
|
1798
|
+
winner.hasLabel = winner.text.length > 0;
|
|
1799
|
+
}
|
|
1800
|
+
if (!winner.testID && loser.testID) winner.testID = loser.testID;
|
|
1801
|
+
if (!winner.accessibilityLabel && loser.accessibilityLabel) winner.accessibilityLabel = loser.accessibilityLabel;
|
|
1802
|
+
return winner;
|
|
1803
|
+
}
|
|
1804
|
+
var deduped = {};
|
|
1805
|
+
for (var di = 0; di < elements.length; di++) {
|
|
1806
|
+
var el = elements[di];
|
|
1807
|
+
var key = el.frame.x + ',' + el.frame.y + ',' + el.frame.width + ',' + el.frame.height;
|
|
1808
|
+
var existing = deduped[key];
|
|
1809
|
+
if (!existing) {
|
|
1810
|
+
deduped[key] = el;
|
|
1811
|
+
} else {
|
|
1812
|
+
var existingIsGeneric = HOST_NAMES.test(existing.component);
|
|
1813
|
+
var newIsGeneric = HOST_NAMES.test(el.component);
|
|
1814
|
+
var winner, loser;
|
|
1815
|
+
if (existingIsGeneric && !newIsGeneric) {
|
|
1816
|
+
winner = el; loser = existing;
|
|
1817
|
+
} else if (!existingIsGeneric && newIsGeneric) {
|
|
1818
|
+
winner = existing; loser = el;
|
|
1819
|
+
} else {
|
|
1820
|
+
// Both generic or both meaningful — prefer the one with more identifiers.
|
|
1821
|
+
if (!existing.testID && el.testID) { winner = el; loser = existing; }
|
|
1822
|
+
else if (!existing.accessibilityLabel && el.accessibilityLabel) { winner = el; loser = existing; }
|
|
1823
|
+
else { winner = existing; loser = el; }
|
|
1824
|
+
}
|
|
1825
|
+
deduped[key] = mergeFields(winner, loser);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
elements = [];
|
|
1829
|
+
for (var dk in deduped) {
|
|
1830
|
+
elements.push(deduped[dk]);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Tag wrappers: pressables that cover >=50% of viewport AND geometrically contain another pressable.
|
|
1834
|
+
// These are typically keyboard-dismiss/full-screen Touchable wrappers — agents should skip them.
|
|
1835
|
+
var viewportArea = (viewportW > 0 && viewportH > 0 && viewportW < 9999 && viewportH < 9999)
|
|
1836
|
+
? viewportW * viewportH : 0;
|
|
1837
|
+
if (viewportArea > 0) {
|
|
1838
|
+
for (var wi = 0; wi < elements.length; wi++) {
|
|
1839
|
+
var we = elements[wi];
|
|
1840
|
+
var weArea = we.frame.width * we.frame.height;
|
|
1841
|
+
if (weArea < viewportArea * 0.5) continue;
|
|
1842
|
+
for (var wj = 0; wj < elements.length; wj++) {
|
|
1843
|
+
if (wj === wi) continue;
|
|
1844
|
+
var other = elements[wj];
|
|
1845
|
+
if (other.frame.x >= we.frame.x &&
|
|
1846
|
+
other.frame.y >= we.frame.y &&
|
|
1847
|
+
other.frame.x + other.frame.width <= we.frame.x + we.frame.width &&
|
|
1848
|
+
other.frame.y + other.frame.height <= we.frame.y + we.frame.height &&
|
|
1849
|
+
other.frame.width * other.frame.height < weArea) {
|
|
1850
|
+
we.isWrapper = true;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Sort top-to-bottom, left-to-right
|
|
1858
|
+
elements.sort(function(a, b) {
|
|
1859
|
+
if (a.center.y !== b.center.y) return a.center.y - b.center.y;
|
|
1860
|
+
return a.center.x - b.center.x;
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
var iconCount = 0;
|
|
1864
|
+
var labeledCount = 0;
|
|
1865
|
+
for (var j = 0; j < elements.length; j++) {
|
|
1866
|
+
if (elements[j].hasLabel) labeledCount++;
|
|
1867
|
+
else iconCount++;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
return {
|
|
1871
|
+
pressableElements: elements,
|
|
1872
|
+
summary: 'Found ' + elements.length + ' pressable elements (' + iconCount + ' icon-only, ' + labeledCount + ' with text labels)'
|
|
1873
|
+
};
|
|
1874
|
+
})()
|
|
1875
|
+
`;
|
|
1876
|
+
const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
|
|
1877
|
+
if (result.success && result.result) {
|
|
1878
|
+
try {
|
|
1879
|
+
const parsed = JSON.parse(result.result);
|
|
1880
|
+
if (parsed.error)
|
|
1881
|
+
return { success: false, error: parsed.error };
|
|
1882
|
+
const pressableElements = parsed.pressableElements || [];
|
|
1883
|
+
// Format as readable text
|
|
1884
|
+
const lines = [parsed.summary, ""];
|
|
1885
|
+
for (let i = 0; i < pressableElements.length; i++) {
|
|
1886
|
+
const el = pressableElements[i];
|
|
1887
|
+
const num = i + 1;
|
|
1888
|
+
const label = el.hasLabel ? `"${el.text}"` : "(icon/image)";
|
|
1889
|
+
const ids = [];
|
|
1890
|
+
if (el.testID)
|
|
1891
|
+
ids.push(`testID="${el.testID}"`);
|
|
1892
|
+
if (el.accessibilityLabel)
|
|
1893
|
+
ids.push(`a11y="${el.accessibilityLabel}"`);
|
|
1894
|
+
const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
|
|
1895
|
+
const inputStr = el.isInput ? " (input)" : "";
|
|
1896
|
+
const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
|
|
1897
|
+
lines.push(`${num}. ${el.component} ${label} — center:(${el.center.x},${el.center.y}) frame:(${el.frame.x},${el.frame.y} ${el.frame.width}x${el.frame.height})${idStr}${inputStr}${wrapperStr}`);
|
|
1898
|
+
if (el.path)
|
|
1899
|
+
lines.push(` path: ${el.path}`);
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
success: true,
|
|
1903
|
+
result: lines.join("\n"),
|
|
1904
|
+
parsedElements: pressableElements
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
catch {
|
|
1908
|
+
// If parsing fails, return original result
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1398
1913
|
/**
|
|
1399
1914
|
* Format enriched elements as an indented tree with tap coordinates in pixels.
|
|
1400
1915
|
* Same tree structure as get_screen_layout but with tap:(x,y) per node.
|
|
@@ -1506,11 +2021,16 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1506
2021
|
}
|
|
1507
2022
|
|
|
1508
2023
|
function extractStyles(style) {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2024
|
+
try {
|
|
2025
|
+
if (!style) return null;
|
|
2026
|
+
const merged = Array.isArray(style)
|
|
2027
|
+
? Object.assign({}, ...style.filter(Boolean).map(s => {
|
|
2028
|
+
try { return typeof s === 'object' ? s : {}; }
|
|
2029
|
+
catch { return {}; }
|
|
2030
|
+
}))
|
|
2031
|
+
: (typeof style === 'object' ? style : {});
|
|
2032
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
2033
|
+
} catch { return { _note: '[Contains animated styles]' }; }
|
|
1514
2034
|
}
|
|
1515
2035
|
|
|
1516
2036
|
function serializeValue(val, depth = 0) {
|
|
@@ -1528,7 +2048,11 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1528
2048
|
if (keys.length > 20) return '[Object(' + keys.length + ' keys)]';
|
|
1529
2049
|
const result = {};
|
|
1530
2050
|
for (const k of keys) {
|
|
1531
|
-
|
|
2051
|
+
try {
|
|
2052
|
+
result[k] = serializeValue(val[k], depth + 1);
|
|
2053
|
+
} catch {
|
|
2054
|
+
result[k] = '[Animated Value]';
|
|
2055
|
+
}
|
|
1532
2056
|
}
|
|
1533
2057
|
return result;
|
|
1534
2058
|
}
|
|
@@ -1596,42 +2120,56 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1596
2120
|
const props = {};
|
|
1597
2121
|
for (const key of Object.keys(fiber.memoizedProps)) {
|
|
1598
2122
|
if (key === 'children') continue;
|
|
1599
|
-
|
|
2123
|
+
try {
|
|
2124
|
+
props[key] = serializeValue(fiber.memoizedProps[key]);
|
|
2125
|
+
} catch {
|
|
2126
|
+
props[key] = '[Animated Value]';
|
|
2127
|
+
}
|
|
1600
2128
|
}
|
|
1601
2129
|
result.props = props;
|
|
1602
2130
|
}
|
|
1603
2131
|
|
|
1604
2132
|
// Style separately for clarity
|
|
1605
|
-
|
|
1606
|
-
|
|
2133
|
+
try {
|
|
2134
|
+
if (fiber.memoizedProps?.style) {
|
|
2135
|
+
result.style = extractStyles(fiber.memoizedProps.style);
|
|
2136
|
+
}
|
|
2137
|
+
} catch {
|
|
2138
|
+
result.style = { _note: '[Contains animated styles]' };
|
|
1607
2139
|
}
|
|
1608
2140
|
|
|
1609
2141
|
// State (for hooks, this is a linked list)
|
|
1610
2142
|
if (includeState && fiber.memoizedState) {
|
|
1611
2143
|
// Simplified hook value serialization
|
|
1612
2144
|
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
|
-
|
|
2145
|
+
try {
|
|
2146
|
+
if (depth > 2) return '[...]';
|
|
2147
|
+
if (val === null || val === undefined) return val;
|
|
2148
|
+
if (typeof val === 'function') return '[Function]';
|
|
2149
|
+
if (typeof val !== 'object') return val;
|
|
2150
|
+
// Skip React internal structures (effects, refs with destroy/create)
|
|
2151
|
+
if (val.create && val.destroy !== undefined) return '[Effect]';
|
|
2152
|
+
if (val.inst && val.deps) return '[Effect]';
|
|
2153
|
+
if (val.current !== undefined && Object.keys(val).length === 1) {
|
|
2154
|
+
// Ref object - just show current value
|
|
2155
|
+
return { current: serializeHookValue(val.current, depth + 1) };
|
|
2156
|
+
}
|
|
2157
|
+
if (Array.isArray(val)) {
|
|
2158
|
+
if (val.length > 5) return '[Array(' + val.length + ')]';
|
|
2159
|
+
return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
|
|
2160
|
+
}
|
|
2161
|
+
const keys = Object.keys(val);
|
|
2162
|
+
if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
|
|
2163
|
+
const result = {};
|
|
2164
|
+
for (const k of keys.slice(0, 10)) {
|
|
2165
|
+
try {
|
|
2166
|
+
result[k] = serializeHookValue(val[k], depth + 1);
|
|
2167
|
+
} catch {
|
|
2168
|
+
result[k] = '[Animated Value]';
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return result;
|
|
2172
|
+
} catch { return '[Animated Value]'; }
|
|
1635
2173
|
}
|
|
1636
2174
|
|
|
1637
2175
|
// For function components with hooks
|
|
@@ -1717,20 +2255,25 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1717
2255
|
}
|
|
1718
2256
|
|
|
1719
2257
|
function extractLayoutStyles(style) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2258
|
+
try {
|
|
2259
|
+
if (!style) return null;
|
|
2260
|
+
const merged = Array.isArray(style)
|
|
2261
|
+
? Object.assign({}, ...style.filter(Boolean).map(s => {
|
|
2262
|
+
try { return typeof s === 'object' ? s : {}; }
|
|
2263
|
+
catch { return {}; }
|
|
2264
|
+
}))
|
|
2265
|
+
: (typeof style === 'object' ? style : {});
|
|
2266
|
+
|
|
2267
|
+
const layout = {};
|
|
2268
|
+
const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
2269
|
+
'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
|
|
2270
|
+
'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
|
|
2271
|
+
'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
|
|
2272
|
+
for (const k of keys) {
|
|
2273
|
+
if (merged[k] !== undefined) layout[k] = merged[k];
|
|
2274
|
+
}
|
|
2275
|
+
return Object.keys(layout).length > 0 ? layout : null;
|
|
2276
|
+
} catch { return null; }
|
|
1734
2277
|
}
|
|
1735
2278
|
|
|
1736
2279
|
const results = [];
|
|
@@ -1839,19 +2382,20 @@ export async function pressElement(options) {
|
|
|
1839
2382
|
const textParam = text ? `'${esc(text)}'` : "null";
|
|
1840
2383
|
const testIDParam = testID ? `'${esc(testID)}'` : "null";
|
|
1841
2384
|
const componentParam = component ? `'${esc(component)}'` : "null";
|
|
1842
|
-
|
|
2385
|
+
// --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
|
|
2386
|
+
const dispatchExpression = `
|
|
1843
2387
|
(function() {
|
|
1844
2388
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1845
2389
|
if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
|
|
1846
2390
|
|
|
1847
2391
|
var roots = [];
|
|
1848
2392
|
if (hook.getFiberRoots) {
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
}
|
|
2393
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
2394
|
+
}
|
|
2395
|
+
if (roots.length === 0 && hook.renderers) {
|
|
2396
|
+
for (var entry of hook.renderers) {
|
|
2397
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
2398
|
+
if (r.length > 0) { roots = r; break; }
|
|
1855
2399
|
}
|
|
1856
2400
|
}
|
|
1857
2401
|
if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
|
|
@@ -1860,6 +2404,7 @@ export async function pressElement(options) {
|
|
|
1860
2404
|
var searchTestID = ${testIDParam};
|
|
1861
2405
|
var searchComponent = ${componentParam};
|
|
1862
2406
|
var targetIndex = ${index};
|
|
2407
|
+
var maxTraversalUp = ${maxTraversalDepth};
|
|
1863
2408
|
|
|
1864
2409
|
function getComponentName(fiber) {
|
|
1865
2410
|
if (!fiber || !fiber.type) return null;
|
|
@@ -1890,15 +2435,105 @@ export async function pressElement(options) {
|
|
|
1890
2435
|
return parts.join('');
|
|
1891
2436
|
}
|
|
1892
2437
|
|
|
1893
|
-
|
|
2438
|
+
function getMeasurable(fiber) {
|
|
2439
|
+
var sn = fiber.stateNode;
|
|
2440
|
+
if (!sn) return null;
|
|
2441
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
2442
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
2443
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
2444
|
+
return sn.canonical.publicInstance;
|
|
2445
|
+
}
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
1894
2448
|
|
|
1895
|
-
|
|
1896
|
-
|
|
2449
|
+
var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
|
|
2450
|
+
|
|
2451
|
+
function isScreenHidden(name, props) {
|
|
2452
|
+
if (!props) return false;
|
|
2453
|
+
if (name === 'RNSScreen' && props['aria-hidden'] === true) return true;
|
|
2454
|
+
if (name === 'MaybeScreen' && props.active === 0) return true;
|
|
2455
|
+
if (name === 'SceneView' && props.focused === false) return true;
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function findMeaningfulAncestorName(fiber) {
|
|
2460
|
+
var cur = fiber.return;
|
|
2461
|
+
var depth = 0;
|
|
2462
|
+
var fallbackName = null;
|
|
2463
|
+
while (cur && depth < 20) {
|
|
2464
|
+
var aname = getComponentName(cur);
|
|
2465
|
+
if (aname && typeof cur.type !== 'string') {
|
|
2466
|
+
if (!fallbackName) fallbackName = aname;
|
|
2467
|
+
if (!RN_PRIMITIVES.test(aname)) return aname;
|
|
2468
|
+
}
|
|
2469
|
+
cur = cur.return;
|
|
2470
|
+
depth++;
|
|
2471
|
+
}
|
|
2472
|
+
return fallbackName;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// Walk UP collecting testID/nativeID from ancestors. Stop at screen boundaries.
|
|
2476
|
+
function collectAncestorTestIDs(fiber, maxUp) {
|
|
2477
|
+
var ids = [];
|
|
2478
|
+
var cur = fiber.return;
|
|
2479
|
+
var d = 0;
|
|
2480
|
+
while (cur && d < maxUp) {
|
|
2481
|
+
var cname = getComponentName(cur);
|
|
2482
|
+
if (cname === 'RNSScreen' || cname === 'MaybeScreen' || cname === 'SceneView') break;
|
|
2483
|
+
var cp = cur.memoizedProps;
|
|
2484
|
+
if (cp) {
|
|
2485
|
+
if (typeof cp.testID === 'string' && cp.testID) ids.push(cp.testID);
|
|
2486
|
+
if (typeof cp.nativeID === 'string' && cp.nativeID) ids.push(cp.nativeID);
|
|
2487
|
+
}
|
|
2488
|
+
cur = cur.return;
|
|
2489
|
+
d++;
|
|
2490
|
+
}
|
|
2491
|
+
return ids;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Find the first measurable host descendant of a fiber.
|
|
2495
|
+
// For inputs, prefer TextInput-specific hosts over generic RCTView.
|
|
2496
|
+
function findFirstHost(fiber, depth, isInput) {
|
|
2497
|
+
if (!fiber || depth > 20) return null;
|
|
2498
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
|
|
2499
|
+
if (isInput) {
|
|
2500
|
+
var hostType = typeof fiber.type === 'string' ? fiber.type : '';
|
|
2501
|
+
if (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1) {
|
|
2502
|
+
return fiber;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return fiber;
|
|
2506
|
+
}
|
|
2507
|
+
var child = fiber.child;
|
|
2508
|
+
var fallback = null;
|
|
2509
|
+
while (child) {
|
|
2510
|
+
var found = findFirstHost(child, depth + 1, isInput);
|
|
2511
|
+
if (found) {
|
|
2512
|
+
if (isInput) {
|
|
2513
|
+
var ft = typeof found.type === 'string' ? found.type : '';
|
|
2514
|
+
if (ft.indexOf('TextInput') !== -1 || ft.indexOf('textinput') !== -1) {
|
|
2515
|
+
return found;
|
|
2516
|
+
}
|
|
2517
|
+
if (!fallback) fallback = found;
|
|
2518
|
+
} else {
|
|
2519
|
+
return found;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
child = child.sibling;
|
|
2523
|
+
}
|
|
2524
|
+
return fallback;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
var hostFibers = [];
|
|
2528
|
+
var tapMeta = [];
|
|
2529
|
+
|
|
2530
|
+
// Phase 1: Walk the entire tree, collect all pressable/input elements
|
|
2531
|
+
function walkFiber(fiber, depth, path) {
|
|
2532
|
+
if (!fiber || depth > 5000) return;
|
|
1897
2533
|
var name = getComponentName(fiber);
|
|
1898
2534
|
var props = fiber.memoizedProps;
|
|
1899
2535
|
|
|
1900
|
-
|
|
1901
|
-
if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
|
|
2536
|
+
if (isScreenHidden(name, props)) return;
|
|
1902
2537
|
|
|
1903
2538
|
var isPressable = props && typeof props.onPress === 'function';
|
|
1904
2539
|
var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
|
|
@@ -1908,33 +2543,28 @@ export async function pressElement(options) {
|
|
|
1908
2543
|
if (isPressable) {
|
|
1909
2544
|
text = extractText(fiber, 0);
|
|
1910
2545
|
} else {
|
|
1911
|
-
// For inputs: use child text, value, defaultValue, or placeholder
|
|
1912
2546
|
var val = typeof props.value === 'string' ? props.value : '';
|
|
1913
2547
|
var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
|
|
1914
2548
|
var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
|
|
1915
2549
|
text = extractText(fiber, 0) || val || defVal || ph;
|
|
1916
2550
|
}
|
|
1917
2551
|
var tid = props.testID || props.nativeID || null;
|
|
1918
|
-
var
|
|
2552
|
+
var meaningful = findMeaningfulAncestorName(fiber);
|
|
2553
|
+
var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
|
|
1919
2554
|
|
|
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,
|
|
2555
|
+
var host = findFirstHost(fiber, 0, isInput);
|
|
2556
|
+
if (host) {
|
|
2557
|
+
hostFibers.push(host);
|
|
2558
|
+
tapMeta.push({
|
|
1933
2559
|
name: name || '(anonymous)',
|
|
2560
|
+
meaningfulComponentName: meaningful || null,
|
|
1934
2561
|
text: text.substring(0, 100),
|
|
1935
2562
|
testID: tid,
|
|
2563
|
+
ancestorTestIDs: ancestorIDs,
|
|
1936
2564
|
path: path.join(' > '),
|
|
1937
|
-
isInput: isInput
|
|
2565
|
+
isInput: isInput,
|
|
2566
|
+
isPressable: isPressable,
|
|
2567
|
+
source: 'direct'
|
|
1938
2568
|
});
|
|
1939
2569
|
}
|
|
1940
2570
|
}
|
|
@@ -1942,151 +2572,353 @@ export async function pressElement(options) {
|
|
|
1942
2572
|
var child = fiber.child;
|
|
1943
2573
|
while (child) {
|
|
1944
2574
|
var childName = getComponentName(child);
|
|
1945
|
-
walkFiber(child, childName ? path.concat([childName]) : path);
|
|
2575
|
+
walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1946
2576
|
child = child.sibling;
|
|
1947
2577
|
}
|
|
1948
2578
|
}
|
|
1949
2579
|
|
|
1950
2580
|
for (var ri = 0; ri < roots.length; ri++) {
|
|
1951
|
-
walkFiber(roots[ri].current, []);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
// Phase 2a:
|
|
1955
|
-
//
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
var
|
|
2581
|
+
walkFiber(roots[ri].current, 0, []);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// Phase 2a: testID on non-pressable wrapper — walk UP or DOWN to pressable/input.
|
|
2585
|
+
// Skipped if Phase 1 already matched via own testID or ancestor testID.
|
|
2586
|
+
if (searchTestID !== null) {
|
|
2587
|
+
var hasEnrichedTestIDMatch = false;
|
|
2588
|
+
for (var di = 0; di < tapMeta.length; di++) {
|
|
2589
|
+
if (tapMeta[di].testID === searchTestID) { hasEnrichedTestIDMatch = true; break; }
|
|
2590
|
+
var aids = tapMeta[di].ancestorTestIDs || [];
|
|
2591
|
+
for (var ai = 0; ai < aids.length; ai++) {
|
|
2592
|
+
if (aids[ai] === searchTestID) { hasEnrichedTestIDMatch = true; break; }
|
|
2593
|
+
}
|
|
2594
|
+
if (hasEnrichedTestIDMatch) break;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
if (!hasEnrichedTestIDMatch) {
|
|
2598
|
+
function findDescendantPressable(fiber, d) {
|
|
2599
|
+
if (!fiber || d > 10) return null;
|
|
2600
|
+
var fp = fiber.memoizedProps;
|
|
2601
|
+
var dIsPressable = fp && typeof fp.onPress === 'function';
|
|
2602
|
+
var dIsInput = !dIsPressable && fp && (typeof fp.onChangeText === 'function' || typeof fp.onFocus === 'function');
|
|
2603
|
+
if (dIsPressable || dIsInput) return { fiber: fiber, isPressable: dIsPressable, isInput: dIsInput };
|
|
2604
|
+
var c = fiber.child;
|
|
2605
|
+
while (c) {
|
|
2606
|
+
var r = findDescendantPressable(c, d + 1);
|
|
2607
|
+
if (r) return r;
|
|
2608
|
+
c = c.sibling;
|
|
2609
|
+
}
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
1959
2612
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2613
|
+
function findByTestID2a(fiber, path) {
|
|
2614
|
+
if (!fiber) return;
|
|
2615
|
+
var name = getComponentName(fiber);
|
|
2616
|
+
var props = fiber.memoizedProps;
|
|
2617
|
+
if (isScreenHidden(name, props)) return;
|
|
2618
|
+
|
|
2619
|
+
var tid = props && (props.testID || props.nativeID || null);
|
|
2620
|
+
if (tid === searchTestID) {
|
|
2621
|
+
var nIsPressable = props && typeof props.onPress === 'function';
|
|
2622
|
+
var nIsInput = !nIsPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
|
|
2623
|
+
|
|
2624
|
+
if (nIsPressable || nIsInput) {
|
|
2625
|
+
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 : ''));
|
|
2626
|
+
var host = findFirstHost(fiber, 0, nIsInput);
|
|
2627
|
+
if (host) {
|
|
2628
|
+
hostFibers.push(host);
|
|
2629
|
+
tapMeta.push({
|
|
2630
|
+
name: name || '(anonymous)',
|
|
2631
|
+
meaningfulComponentName: findMeaningfulAncestorName(fiber) || null,
|
|
2632
|
+
text: text.substring(0, 100),
|
|
2633
|
+
testID: searchTestID,
|
|
2634
|
+
ancestorTestIDs: [],
|
|
2635
|
+
path: path.join(' > '),
|
|
2636
|
+
isInput: nIsInput,
|
|
2637
|
+
isPressable: nIsPressable,
|
|
2638
|
+
source: 'testID-direct'
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
} else {
|
|
2642
|
+
var foundAncestor = false;
|
|
2643
|
+
var parent = fiber.return;
|
|
2644
|
+
var d = 0;
|
|
2645
|
+
while (parent && d < maxTraversalUp) {
|
|
2646
|
+
var pp = parent.memoizedProps;
|
|
2647
|
+
var pIsPressable = pp && typeof pp.onPress === 'function';
|
|
2648
|
+
var pIsInput = !pIsPressable && pp && (typeof pp.onChangeText === 'function' || typeof pp.onFocus === 'function');
|
|
2649
|
+
if (pIsPressable || pIsInput) {
|
|
2650
|
+
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 : ''));
|
|
2651
|
+
var host = findFirstHost(parent, 0, pIsInput);
|
|
2652
|
+
if (host) {
|
|
2653
|
+
hostFibers.push(host);
|
|
2654
|
+
tapMeta.push({
|
|
2655
|
+
name: name || '(anonymous)',
|
|
2656
|
+
meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
|
|
2657
|
+
text: pText.substring(0, 100),
|
|
2658
|
+
testID: pp.testID || pp.nativeID || searchTestID,
|
|
2659
|
+
ancestorTestIDs: [],
|
|
2660
|
+
path: path.join(' > '),
|
|
2661
|
+
isInput: pIsInput,
|
|
2662
|
+
isPressable: pIsPressable,
|
|
2663
|
+
source: 'testID-ancestor'
|
|
2664
|
+
});
|
|
2665
|
+
foundAncestor = true;
|
|
2666
|
+
}
|
|
2667
|
+
break;
|
|
2668
|
+
}
|
|
2669
|
+
parent = parent.return;
|
|
2670
|
+
d++;
|
|
2671
|
+
}
|
|
1965
2672
|
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2673
|
+
if (!foundAncestor) {
|
|
2674
|
+
var desc = findDescendantPressable(fiber, 0);
|
|
2675
|
+
if (desc) {
|
|
2676
|
+
var dp = desc.fiber.memoizedProps;
|
|
2677
|
+
var dText = desc.isPressable ? extractText(desc.fiber, 0) : (extractText(desc.fiber, 0) || (typeof dp.value === 'string' ? dp.value : '') || (typeof dp.defaultValue === 'string' ? dp.defaultValue : '') || (typeof dp.placeholder === 'string' ? dp.placeholder : ''));
|
|
2678
|
+
var dhost = findFirstHost(desc.fiber, 0, desc.isInput);
|
|
2679
|
+
if (dhost) {
|
|
2680
|
+
hostFibers.push(dhost);
|
|
2681
|
+
tapMeta.push({
|
|
2682
|
+
name: getComponentName(desc.fiber) || '(anonymous)',
|
|
2683
|
+
meaningfulComponentName: findMeaningfulAncestorName(desc.fiber) || null,
|
|
2684
|
+
text: dText.substring(0, 100),
|
|
2685
|
+
testID: dp.testID || dp.nativeID || searchTestID,
|
|
2686
|
+
ancestorTestIDs: [searchTestID],
|
|
2687
|
+
path: path.join(' > '),
|
|
2688
|
+
isInput: desc.isInput,
|
|
2689
|
+
isPressable: desc.isPressable,
|
|
2690
|
+
source: 'testID-descendant'
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
var child = fiber.child;
|
|
2698
|
+
while (child) {
|
|
2699
|
+
var childName = getComponentName(child);
|
|
2700
|
+
findByTestID2a(child, childName ? path.concat([childName]) : path);
|
|
2701
|
+
child = child.sibling;
|
|
2702
|
+
}
|
|
1969
2703
|
}
|
|
1970
|
-
var
|
|
1971
|
-
|
|
1972
|
-
var childName = getComponentName(child);
|
|
1973
|
-
findByTestID(child, childName ? path.concat([childName]) : path);
|
|
1974
|
-
child = child.sibling;
|
|
2704
|
+
for (var ri2a = 0; ri2a < roots.length; ri2a++) {
|
|
2705
|
+
findByTestID2a(roots[ri2a].current, []);
|
|
1975
2706
|
}
|
|
1976
2707
|
}
|
|
2708
|
+
}
|
|
1977
2709
|
|
|
1978
|
-
|
|
1979
|
-
|
|
2710
|
+
// Phase 2b: component name on non-pressable node — walk UP or DOWN to pressable parent.
|
|
2711
|
+
// Skipped if Phase 1 already matched via own name or meaningfulComponentName.
|
|
2712
|
+
if (searchComponent !== null) {
|
|
2713
|
+
var scLower = searchComponent.toLowerCase();
|
|
2714
|
+
var hasEnrichedComponentMatch = false;
|
|
2715
|
+
for (var ci = 0; ci < tapMeta.length; ci++) {
|
|
2716
|
+
var cn = (tapMeta[ci].name || '').toLowerCase();
|
|
2717
|
+
var cm = (tapMeta[ci].meaningfulComponentName || '').toLowerCase();
|
|
2718
|
+
if (cn.indexOf(scLower) !== -1 || cm.indexOf(scLower) !== -1) {
|
|
2719
|
+
hasEnrichedComponentMatch = true; break;
|
|
2720
|
+
}
|
|
1980
2721
|
}
|
|
1981
2722
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
var text = extractText(tCandidate.fiber, 0);
|
|
1993
|
-
if (tIsInput) {
|
|
1994
|
-
var val = typeof tProps.value === 'string' ? tProps.value : '';
|
|
1995
|
-
var defVal = typeof tProps.defaultValue === 'string' ? tProps.defaultValue : '';
|
|
1996
|
-
var ph = typeof tProps.placeholder === 'string' ? tProps.placeholder : '';
|
|
1997
|
-
text = text || val || defVal || ph;
|
|
2723
|
+
if (!hasEnrichedComponentMatch) {
|
|
2724
|
+
function findDescendantPressableOnly(fiber, d) {
|
|
2725
|
+
if (!fiber || d > 10) return null;
|
|
2726
|
+
var fp = fiber.memoizedProps;
|
|
2727
|
+
if (fp && typeof fp.onPress === 'function') return fiber;
|
|
2728
|
+
var c = fiber.child;
|
|
2729
|
+
while (c) {
|
|
2730
|
+
var r = findDescendantPressableOnly(c, d + 1);
|
|
2731
|
+
if (r) return r;
|
|
2732
|
+
c = c.sibling;
|
|
1998
2733
|
}
|
|
1999
|
-
|
|
2000
|
-
fiber: tCandidate.fiber,
|
|
2001
|
-
name: tCandidate.name,
|
|
2002
|
-
text: text.substring(0, 100),
|
|
2003
|
-
testID: searchTestID,
|
|
2004
|
-
path: tCandidate.path,
|
|
2005
|
-
isInput: tIsInput
|
|
2006
|
-
});
|
|
2007
|
-
continue;
|
|
2734
|
+
return null;
|
|
2008
2735
|
}
|
|
2009
2736
|
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
var
|
|
2020
|
-
|
|
2021
|
-
var
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2737
|
+
function findByName2b(fiber, path) {
|
|
2738
|
+
if (!fiber) return;
|
|
2739
|
+
var name = getComponentName(fiber);
|
|
2740
|
+
var props = fiber.memoizedProps;
|
|
2741
|
+
if (isScreenHidden(name, props)) return;
|
|
2742
|
+
|
|
2743
|
+
if (name && name.toLowerCase().indexOf(scLower) !== -1) {
|
|
2744
|
+
var foundAncestor = false;
|
|
2745
|
+
var parent = fiber.return;
|
|
2746
|
+
var d = 0;
|
|
2747
|
+
while (parent && d < maxTraversalUp) {
|
|
2748
|
+
var pp = parent.memoizedProps;
|
|
2749
|
+
if (pp && typeof pp.onPress === 'function') {
|
|
2750
|
+
var text = extractText(parent, 0);
|
|
2751
|
+
var host = findFirstHost(parent, 0, false);
|
|
2752
|
+
if (host) {
|
|
2753
|
+
hostFibers.push(host);
|
|
2754
|
+
tapMeta.push({
|
|
2755
|
+
name: name,
|
|
2756
|
+
meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
|
|
2757
|
+
text: text.substring(0, 100),
|
|
2758
|
+
testID: pp.testID || pp.nativeID || null,
|
|
2759
|
+
ancestorTestIDs: [],
|
|
2760
|
+
path: path.join(' > '),
|
|
2761
|
+
isInput: false,
|
|
2762
|
+
isPressable: true,
|
|
2763
|
+
source: 'component-ancestor'
|
|
2764
|
+
});
|
|
2765
|
+
foundAncestor = true;
|
|
2766
|
+
}
|
|
2767
|
+
break;
|
|
2768
|
+
}
|
|
2769
|
+
parent = parent.return;
|
|
2770
|
+
d++;
|
|
2025
2771
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2772
|
+
|
|
2773
|
+
if (!foundAncestor) {
|
|
2774
|
+
var descFiber = findDescendantPressableOnly(fiber, 0);
|
|
2775
|
+
if (descFiber) {
|
|
2776
|
+
var dp = descFiber.memoizedProps;
|
|
2777
|
+
var dText = extractText(descFiber, 0);
|
|
2778
|
+
var dhost = findFirstHost(descFiber, 0, false);
|
|
2779
|
+
if (dhost) {
|
|
2780
|
+
hostFibers.push(dhost);
|
|
2781
|
+
tapMeta.push({
|
|
2782
|
+
name: getComponentName(descFiber) || '(anonymous)',
|
|
2783
|
+
meaningfulComponentName: name,
|
|
2784
|
+
text: dText.substring(0, 100),
|
|
2785
|
+
testID: dp.testID || dp.nativeID || null,
|
|
2786
|
+
ancestorTestIDs: [],
|
|
2787
|
+
path: path.join(' > '),
|
|
2788
|
+
isInput: false,
|
|
2789
|
+
isPressable: true,
|
|
2790
|
+
source: 'component-descendant'
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
var child = fiber.child;
|
|
2797
|
+
while (child) {
|
|
2798
|
+
var childName = getComponentName(child);
|
|
2799
|
+
findByName2b(child, childName ? path.concat([childName]) : path);
|
|
2800
|
+
child = child.sibling;
|
|
2035
2801
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2802
|
+
}
|
|
2803
|
+
for (var ri2b = 0; ri2b < roots.length; ri2b++) {
|
|
2804
|
+
findByName2b(roots[ri2b].current, []);
|
|
2038
2805
|
}
|
|
2039
2806
|
}
|
|
2040
2807
|
}
|
|
2041
2808
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2809
|
+
if (hostFibers.length === 0) {
|
|
2810
|
+
var criteria = [];
|
|
2811
|
+
if (searchText !== null) criteria.push('text="' + searchText + '"');
|
|
2812
|
+
if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
|
|
2813
|
+
if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
|
|
2814
|
+
return { error: 'No pressable or focusable elements found. Searched for: ' + criteria.join(', ') };
|
|
2815
|
+
}
|
|
2046
2816
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
|
|
2817
|
+
// Store host fibers and metadata globally for step 2, dispatch measureInWindow
|
|
2818
|
+
globalThis.__tapHostFibers = hostFibers;
|
|
2819
|
+
globalThis.__tapMeta = tapMeta;
|
|
2820
|
+
globalThis.__tapMeasurements = new Array(hostFibers.length).fill(null);
|
|
2052
2821
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2822
|
+
for (var mi = 0; mi < hostFibers.length; mi++) {
|
|
2823
|
+
try {
|
|
2824
|
+
(function(idx) {
|
|
2825
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
2826
|
+
globalThis.__tapMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
2827
|
+
});
|
|
2828
|
+
})(mi);
|
|
2829
|
+
} catch(e) {}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
return { count: hostFibers.length };
|
|
2833
|
+
})()
|
|
2834
|
+
`;
|
|
2835
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, options.device);
|
|
2836
|
+
if (!dispatchResult.success)
|
|
2837
|
+
return dispatchResult;
|
|
2838
|
+
try {
|
|
2839
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
2840
|
+
if (parsed.error)
|
|
2841
|
+
return { success: false, error: parsed.error };
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
/* ignore */
|
|
2845
|
+
}
|
|
2846
|
+
// Wait for measureInWindow callbacks
|
|
2847
|
+
await delay(300);
|
|
2848
|
+
// --- Step 2: Read measurements, filter visible, match by query ---
|
|
2849
|
+
const resolveExpression = `
|
|
2850
|
+
(function() {
|
|
2851
|
+
var hostFibers = globalThis.__tapHostFibers;
|
|
2852
|
+
var meta = globalThis.__tapMeta;
|
|
2853
|
+
var measurements = globalThis.__tapMeasurements;
|
|
2854
|
+
globalThis.__tapHostFibers = null;
|
|
2855
|
+
globalThis.__tapMeta = null;
|
|
2856
|
+
globalThis.__tapMeasurements = null;
|
|
2857
|
+
|
|
2858
|
+
if (!hostFibers || !measurements || !meta) {
|
|
2859
|
+
return { error: 'No measurement data. Dispatch step may have failed.' };
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
var searchText = ${textParam};
|
|
2863
|
+
var searchTestID = ${testIDParam};
|
|
2864
|
+
var searchComponent = ${componentParam};
|
|
2865
|
+
var targetIndex = ${index};
|
|
2866
|
+
|
|
2867
|
+
// Determine viewport bounds
|
|
2868
|
+
var viewportW = 9999, viewportH = 9999;
|
|
2869
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
2870
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
|
|
2871
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
2872
|
+
viewportW = measurements[v].width;
|
|
2873
|
+
viewportH = measurements[v].height + measurements[v].y;
|
|
2874
|
+
break;
|
|
2062
2875
|
}
|
|
2876
|
+
}
|
|
2063
2877
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
depth++;
|
|
2878
|
+
// Filter visible and match
|
|
2879
|
+
var matches = [];
|
|
2880
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
2881
|
+
var m = measurements[i];
|
|
2882
|
+
if (!m) continue;
|
|
2883
|
+
|
|
2884
|
+
// Visibility filter: positive dimensions, within viewport
|
|
2885
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
2886
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
2887
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
2888
|
+
|
|
2889
|
+
var info = meta[i];
|
|
2890
|
+
|
|
2891
|
+
// Match by query — OR across own and enriched identifiers
|
|
2892
|
+
var matched = true;
|
|
2893
|
+
if (searchText !== null) {
|
|
2894
|
+
matched = matched && info.text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
|
|
2895
|
+
}
|
|
2896
|
+
if (searchTestID !== null) {
|
|
2897
|
+
var ownTidMatch = info.testID === searchTestID;
|
|
2898
|
+
var aTids = info.ancestorTestIDs || [];
|
|
2899
|
+
var ancestorTidMatch = false;
|
|
2900
|
+
for (var ti = 0; ti < aTids.length; ti++) {
|
|
2901
|
+
if (aTids[ti] === searchTestID) { ancestorTidMatch = true; break; }
|
|
2089
2902
|
}
|
|
2903
|
+
matched = matched && (ownTidMatch || ancestorTidMatch);
|
|
2904
|
+
}
|
|
2905
|
+
if (searchComponent !== null) {
|
|
2906
|
+
var scq = searchComponent.toLowerCase();
|
|
2907
|
+
var ownNameMatch = (info.name || '').toLowerCase().indexOf(scq) !== -1;
|
|
2908
|
+
var meaningfulMatch = (info.meaningfulComponentName || '').toLowerCase().indexOf(scq) !== -1;
|
|
2909
|
+
matched = matched && (ownNameMatch || meaningfulMatch);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
if (matched) {
|
|
2913
|
+
matches.push({
|
|
2914
|
+
name: info.name,
|
|
2915
|
+
text: info.text,
|
|
2916
|
+
testID: info.testID,
|
|
2917
|
+
path: info.path,
|
|
2918
|
+
isInput: info.isInput,
|
|
2919
|
+
x: Math.round(m.x + m.width / 2),
|
|
2920
|
+
y: Math.round(m.y + m.height / 2)
|
|
2921
|
+
});
|
|
2090
2922
|
}
|
|
2091
2923
|
}
|
|
2092
2924
|
|
|
@@ -2095,12 +2927,12 @@ export async function pressElement(options) {
|
|
|
2095
2927
|
if (searchText !== null) criteria.push('text="' + searchText + '"');
|
|
2096
2928
|
if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
|
|
2097
2929
|
if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
|
|
2098
|
-
return { error: 'No pressable or focusable elements found matching: ' + criteria.join(', ') };
|
|
2930
|
+
return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
|
|
2099
2931
|
}
|
|
2100
2932
|
|
|
2101
2933
|
if (targetIndex >= matches.length) {
|
|
2102
2934
|
return {
|
|
2103
|
-
error: 'Found ' + matches.length + ' match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
|
|
2935
|
+
error: 'Found ' + matches.length + ' visible match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
|
|
2104
2936
|
matches: matches.map(function(m, i) {
|
|
2105
2937
|
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
2106
2938
|
})
|
|
@@ -2108,137 +2940,28 @@ export async function pressElement(options) {
|
|
|
2108
2940
|
}
|
|
2109
2941
|
|
|
2110
2942
|
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 };
|
|
2943
|
+
var result = {
|
|
2944
|
+
success: true,
|
|
2945
|
+
pressed: target.name,
|
|
2946
|
+
matchIndex: targetIndex,
|
|
2947
|
+
totalMatches: matches.length,
|
|
2948
|
+
text: target.text,
|
|
2949
|
+
testID: target.testID,
|
|
2950
|
+
path: target.path,
|
|
2951
|
+
isInput: target.isInput,
|
|
2952
|
+
x: target.x,
|
|
2953
|
+
y: target.y,
|
|
2954
|
+
unit: 'points'
|
|
2955
|
+
};
|
|
2956
|
+
if (matches.length > 1) {
|
|
2957
|
+
result.allMatches = matches.map(function(m, i) {
|
|
2958
|
+
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
2959
|
+
});
|
|
2238
2960
|
}
|
|
2961
|
+
return result;
|
|
2239
2962
|
})()
|
|
2240
2963
|
`;
|
|
2241
|
-
return executeInApp(
|
|
2964
|
+
return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, options.device);
|
|
2242
2965
|
}
|
|
2243
2966
|
// ============================================================================
|
|
2244
2967
|
// Coordinate-Based Element Inspection (via DevTools Inspector API)
|