react-native-ai-debugger 1.1.6 → 1.4.21
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/LICENSE +1 -1
- package/README.md +371 -42
- package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
- package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
- package/build/__tests__/helpers/tap-test-helpers.js +130 -0
- package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
- package/build/__tests__/integration/connection-health.test.js +12 -6
- package/build/__tests__/integration/connection-health.test.js.map +1 -1
- package/build/__tests__/integration/execute-in-app.test.js +2 -1
- package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
- package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
- package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
- package/build/__tests__/integration/tap-strategies.test.js +329 -0
- package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
- package/build/__tests__/integration/tools.test.js +9 -10
- package/build/__tests__/integration/tools.test.js.map +1 -1
- package/build/__tests__/unit/burst.test.d.ts +2 -0
- package/build/__tests__/unit/burst.test.d.ts.map +1 -0
- package/build/__tests__/unit/burst.test.js +50 -0
- package/build/__tests__/unit/burst.test.js.map +1 -0
- package/build/__tests__/unit/connection-health.test.js +26 -16
- package/build/__tests__/unit/connection-health.test.js.map +1 -1
- package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
- package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
- package/build/__tests__/unit/fingerprint.test.js +89 -0
- package/build/__tests__/unit/fingerprint.test.js.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
- package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
- package/build/__tests__/unit/imageBuffer.test.js +128 -0
- package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
- package/build/__tests__/unit/logs.test.js +9 -0
- package/build/__tests__/unit/logs.test.js.map +1 -1
- package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
- package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
- package/build/__tests__/unit/networkInterceptor.test.js +104 -0
- package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
- package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenshot-diff.test.js +97 -0
- package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
- package/build/__tests__/unit/tap.test.js +430 -25
- package/build/__tests__/unit/tap.test.js.map +1 -1
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +64 -29
- package/build/core/android.js.map +1 -1
- package/build/core/appDetection.d.ts +8 -0
- package/build/core/appDetection.d.ts.map +1 -0
- package/build/core/appDetection.js +91 -0
- package/build/core/appDetection.js.map +1 -0
- package/build/core/bundle.d.ts.map +1 -1
- package/build/core/bundle.js +15 -23
- package/build/core/bundle.js.map +1 -1
- package/build/core/config.d.ts +8 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +27 -0
- package/build/core/config.js.map +1 -0
- package/build/core/connection.d.ts +47 -3
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +519 -124
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +57 -14
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +936 -208
- package/build/core/executor.js.map +1 -1
- package/build/core/fingerprint.d.ts +4 -0
- package/build/core/fingerprint.d.ts.map +1 -0
- package/build/core/fingerprint.js +47 -0
- package/build/core/fingerprint.js.map +1 -0
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +27 -3
- package/build/core/guides.js.map +1 -1
- package/build/core/httpServer.js.map +1 -1
- package/build/core/imageBuffer.d.ts +37 -0
- package/build/core/imageBuffer.d.ts.map +1 -0
- package/build/core/imageBuffer.js +58 -0
- package/build/core/imageBuffer.js.map +1 -0
- package/build/core/index.d.ts +21 -17
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +21 -19
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +36 -7
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +553 -323
- package/build/core/ios.js.map +1 -1
- package/build/core/license.d.ts +38 -0
- package/build/core/license.d.ts.map +1 -0
- package/build/core/license.js +265 -0
- package/build/core/license.js.map +1 -0
- package/build/core/logbox.d.ts +51 -0
- package/build/core/logbox.d.ts.map +1 -0
- package/build/core/logbox.js +258 -0
- package/build/core/logbox.js.map +1 -0
- package/build/core/logs.d.ts +1 -0
- package/build/core/logs.d.ts.map +1 -1
- package/build/core/logs.js +5 -0
- package/build/core/logs.js.map +1 -1
- package/build/core/metro.d.ts +8 -0
- package/build/core/metro.d.ts.map +1 -1
- package/build/core/metro.js +28 -0
- package/build/core/metro.js.map +1 -1
- package/build/core/networkInterceptor.d.ts +29 -2
- package/build/core/networkInterceptor.d.ts.map +1 -1
- package/build/core/networkInterceptor.js +195 -191
- package/build/core/networkInterceptor.js.map +1 -1
- package/build/core/ocr.d.ts +8 -3
- package/build/core/ocr.d.ts.map +1 -1
- package/build/core/ocr.js +116 -15
- package/build/core/ocr.js.map +1 -1
- package/build/core/posthog.d.ts +5 -0
- package/build/core/posthog.d.ts.map +1 -0
- package/build/core/posthog.js +46 -0
- package/build/core/posthog.js.map +1 -0
- package/build/core/sdkBridge.d.ts +67 -0
- package/build/core/sdkBridge.d.ts.map +1 -0
- package/build/core/sdkBridge.js +148 -0
- package/build/core/sdkBridge.js.map +1 -0
- package/build/core/state.d.ts +25 -5
- package/build/core/state.d.ts.map +1 -1
- package/build/core/state.js +79 -10
- package/build/core/state.js.map +1 -1
- package/build/core/telemetry.d.ts +19 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +163 -8
- package/build/core/telemetry.js.map +1 -1
- package/build/core/types.d.ts +11 -0
- package/build/core/types.d.ts.map +1 -1
- package/build/index.js +1302 -302
- package/build/index.js.map +1 -1
- package/build/pro/screenshot-diff.d.ts +10 -0
- package/build/pro/screenshot-diff.d.ts.map +1 -0
- package/build/pro/screenshot-diff.js +38 -0
- package/build/pro/screenshot-diff.js.map +1 -0
- package/build/pro/tap.d.ts +71 -2
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +797 -124
- package/build/pro/tap.js.map +1 -1
- package/build/tools/accountTools.d.ts +44 -0
- package/build/tools/accountTools.d.ts.map +1 -0
- package/build/tools/accountTools.js +217 -0
- package/build/tools/accountTools.js.map +1 -0
- package/package.json +9 -7
- package/build/core/httpServer.js +0 -2459
- package/build/core/httpServerProcess.js +0 -153
- package/build/httpServerStandalone.js +0 -31
package/build/core/executor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { pendingExecutions, getNextMessageId, connectedApps } from "./state.js";
|
|
3
|
-
import { getFirstConnectedApp, connectToDevice } from "./connection.js";
|
|
3
|
+
import { getFirstConnectedApp, getConnectedAppByDevice, connectToDevice } from "./connection.js";
|
|
4
4
|
import { fetchDevices, selectMainDevice, scanMetroPorts } from "./metro.js";
|
|
5
5
|
import { DEFAULT_RECONNECTION_CONFIG, cancelReconnectionTimer } from "./connectionState.js";
|
|
6
6
|
// Hermes runtime compatibility: polyfill for 'global' which doesn't exist in Hermes
|
|
@@ -80,6 +80,16 @@ export function validateAndPreprocessExpression(expression) {
|
|
|
80
80
|
"`global.__result = null; myAsyncFn().then(r => global.__result = r)`"
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
// Check for require() calls that don't work in Hermes Runtime.evaluate
|
|
84
|
+
if (/\brequire\s*\(/.test(trimmed)) {
|
|
85
|
+
return {
|
|
86
|
+
valid: false,
|
|
87
|
+
expression: cleaned,
|
|
88
|
+
error: "require() is not available in Hermes Runtime.evaluate. " +
|
|
89
|
+
"Modules cannot be imported at runtime. Only pre-existing global variables are accessible. " +
|
|
90
|
+
"Use list_debug_globals to discover available globals, or add `globalThis.__MY_VAR__ = myModule;` in your app code."
|
|
91
|
+
};
|
|
92
|
+
}
|
|
83
93
|
return {
|
|
84
94
|
valid: true,
|
|
85
95
|
expression: cleaned
|
|
@@ -157,7 +167,31 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
|
|
|
157
167
|
return new Promise((resolve) => {
|
|
158
168
|
const timeoutId = setTimeout(() => {
|
|
159
169
|
pendingExecutions.delete(currentMessageId);
|
|
160
|
-
|
|
170
|
+
const wsState = app.ws.readyState === WebSocket.OPEN ? "OPEN"
|
|
171
|
+
: app.ws.readyState === WebSocket.CLOSED ? "CLOSED"
|
|
172
|
+
: app.ws.readyState === WebSocket.CLOSING ? "CLOSING"
|
|
173
|
+
: "CONNECTING";
|
|
174
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
175
|
+
const pageId = app.deviceInfo.id || "unknown";
|
|
176
|
+
const truncatedExpr = cleanedExpression.length > 100
|
|
177
|
+
? cleanedExpression.substring(0, 100) + "..."
|
|
178
|
+
: cleanedExpression;
|
|
179
|
+
const errorMessage = [
|
|
180
|
+
"Timeout: Expression took too long to evaluate.",
|
|
181
|
+
"",
|
|
182
|
+
`Connection state: ws=${wsState}, device="${deviceName}", platform=${app.platform}, pageId=${pageId}`,
|
|
183
|
+
`Expression (truncated): ${truncatedExpr}`,
|
|
184
|
+
"",
|
|
185
|
+
"This usually means the JavaScript execution context became unresponsive or the CDP page is stale.",
|
|
186
|
+
"",
|
|
187
|
+
"Recovery steps (try in order):",
|
|
188
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
189
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
190
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
191
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
192
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
193
|
+
].join("\n");
|
|
194
|
+
resolve({ success: false, error: errorMessage });
|
|
161
195
|
}, TIMEOUT_MS);
|
|
162
196
|
pendingExecutions.set(currentMessageId, { resolve, timeoutId });
|
|
163
197
|
try {
|
|
@@ -184,17 +218,17 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
|
|
|
184
218
|
});
|
|
185
219
|
}
|
|
186
220
|
// Execute JavaScript in the connected React Native app with retry logic
|
|
187
|
-
export async function executeInApp(expression, awaitPromise = true, options = {}) {
|
|
221
|
+
export async function executeInApp(expression, awaitPromise = true, options = {}, device) {
|
|
188
222
|
const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
|
|
189
223
|
let lastError;
|
|
190
224
|
let preferredPort;
|
|
191
225
|
// Get preferred port from current connection if available
|
|
192
|
-
const currentApp =
|
|
226
|
+
const currentApp = getConnectedAppByDevice(device);
|
|
193
227
|
if (currentApp) {
|
|
194
228
|
preferredPort = currentApp.port;
|
|
195
229
|
}
|
|
196
230
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
197
|
-
const app =
|
|
231
|
+
const app = getConnectedAppByDevice(device);
|
|
198
232
|
// No connection - try to reconnect if enabled
|
|
199
233
|
if (!app) {
|
|
200
234
|
if (autoReconnect && attempt < maxRetries) {
|
|
@@ -262,11 +296,20 @@ export async function executeInApp(expression, awaitPromise = true, options = {}
|
|
|
262
296
|
}
|
|
263
297
|
return {
|
|
264
298
|
success: false,
|
|
265
|
-
error: lastError ??
|
|
299
|
+
error: lastError ?? [
|
|
300
|
+
"Execution failed after all retries. Connection may be stale.",
|
|
301
|
+
"",
|
|
302
|
+
"Recovery steps (try in order):",
|
|
303
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
304
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
305
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
306
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
307
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
308
|
+
].join("\n")
|
|
266
309
|
};
|
|
267
310
|
}
|
|
268
311
|
// List globally available debugging objects in the app
|
|
269
|
-
export async function listDebugGlobals() {
|
|
312
|
+
export async function listDebugGlobals(device) {
|
|
270
313
|
const expression = `
|
|
271
314
|
(function() {
|
|
272
315
|
const globals = Object.keys(globalThis);
|
|
@@ -282,10 +325,10 @@ export async function listDebugGlobals() {
|
|
|
282
325
|
return categories;
|
|
283
326
|
})()
|
|
284
327
|
`;
|
|
285
|
-
return executeInApp(expression, false);
|
|
328
|
+
return executeInApp(expression, false, {}, device);
|
|
286
329
|
}
|
|
287
330
|
// Inspect a global object to see its properties and types
|
|
288
|
-
export async function inspectGlobal(objectName) {
|
|
331
|
+
export async function inspectGlobal(objectName, device) {
|
|
289
332
|
const expression = `
|
|
290
333
|
(function() {
|
|
291
334
|
const obj = ${objectName};
|
|
@@ -305,14 +348,15 @@ export async function inspectGlobal(objectName) {
|
|
|
305
348
|
return result;
|
|
306
349
|
})()
|
|
307
350
|
`;
|
|
308
|
-
return executeInApp(expression, false);
|
|
351
|
+
return executeInApp(expression, false, {}, device);
|
|
309
352
|
}
|
|
310
|
-
// Reload the React Native app using __ReactRefresh
|
|
353
|
+
// Reload the React Native app using __ReactRefresh
|
|
354
|
+
// Note: Page.reload CDP method may work on Bridgeless targets (via HostAgent) — not yet tested
|
|
311
355
|
// Uses fire-and-forget: sends the reload command without waiting for a response,
|
|
312
356
|
// since the JS context is destroyed during reload and would always timeout.
|
|
313
|
-
export async function reloadApp() {
|
|
357
|
+
export async function reloadApp(device) {
|
|
314
358
|
// Get current connection info before reload
|
|
315
|
-
let app =
|
|
359
|
+
let app = getConnectedAppByDevice(device);
|
|
316
360
|
// Auto-connect if no connection exists
|
|
317
361
|
if (!app) {
|
|
318
362
|
console.error("[rn-ai-debugger] No connection for reload, attempting auto-connect...");
|
|
@@ -332,7 +376,7 @@ export async function reloadApp() {
|
|
|
332
376
|
try {
|
|
333
377
|
await connectToDevice(mainDevice, port);
|
|
334
378
|
console.error(`[rn-ai-debugger] Auto-connected to ${mainDevice.title} on port ${port}`);
|
|
335
|
-
app =
|
|
379
|
+
app = getConnectedAppByDevice(device);
|
|
336
380
|
break;
|
|
337
381
|
}
|
|
338
382
|
catch (error) {
|
|
@@ -366,7 +410,21 @@ export async function reloadApp() {
|
|
|
366
410
|
})()`;
|
|
367
411
|
try {
|
|
368
412
|
if (app.ws.readyState !== WebSocket.OPEN) {
|
|
369
|
-
|
|
413
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
414
|
+
return {
|
|
415
|
+
success: false,
|
|
416
|
+
error: [
|
|
417
|
+
`WebSocket connection is not open (device="${deviceName}", platform=${app.platform}).`,
|
|
418
|
+
"The CDP page may be stale or the app has crashed.",
|
|
419
|
+
"",
|
|
420
|
+
"Recovery steps (try in order):",
|
|
421
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
422
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
423
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
424
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
425
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
426
|
+
].join("\n")
|
|
427
|
+
};
|
|
370
428
|
}
|
|
371
429
|
// Send without registering a pending execution — fire and forget
|
|
372
430
|
const messageId = getNextMessageId();
|
|
@@ -391,11 +449,10 @@ export async function reloadApp() {
|
|
|
391
449
|
try {
|
|
392
450
|
// Wait for app to reload (give it time to restart JS context)
|
|
393
451
|
await delay(2000);
|
|
394
|
-
//
|
|
395
|
-
|
|
452
|
+
// Find and close only the targeted device's connection (not all devices on this port)
|
|
453
|
+
const targetDeviceId = app.deviceInfo.id;
|
|
396
454
|
for (const [key, connectedApp] of connectedApps.entries()) {
|
|
397
|
-
if (connectedApp.
|
|
398
|
-
// Cancel any pending reconnection timer BEFORE closing
|
|
455
|
+
if (connectedApp.deviceInfo.id === targetDeviceId) {
|
|
399
456
|
cancelReconnectionTimer(key);
|
|
400
457
|
try {
|
|
401
458
|
connectedApp.ws.close();
|
|
@@ -404,23 +461,23 @@ export async function reloadApp() {
|
|
|
404
461
|
// Ignore close errors
|
|
405
462
|
}
|
|
406
463
|
connectedApps.delete(key);
|
|
464
|
+
break;
|
|
407
465
|
}
|
|
408
466
|
}
|
|
409
467
|
// Small delay to ensure cleanup
|
|
410
468
|
await delay(500);
|
|
411
|
-
// Reconnect
|
|
412
|
-
// We're doing a manual reconnection here, so we don't want the auto-reconnect
|
|
413
|
-
// system to also try reconnecting and compete with us
|
|
469
|
+
// Reconnect only the reloaded device (not all devices on the port)
|
|
414
470
|
const devices = await fetchDevices(port);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
471
|
+
const targetDevice = devices.find(d => d.id === targetDeviceId)
|
|
472
|
+
|| devices.find(d => d.deviceName === app.deviceInfo.deviceName);
|
|
473
|
+
if (targetDevice) {
|
|
474
|
+
await connectToDevice(targetDevice, port, {
|
|
418
475
|
isReconnection: false,
|
|
419
476
|
reconnectionConfig: { ...DEFAULT_RECONNECTION_CONFIG, enabled: false }
|
|
420
477
|
});
|
|
421
478
|
return {
|
|
422
479
|
success: true,
|
|
423
|
-
result: `App reloaded and reconnected to ${
|
|
480
|
+
result: `App reloaded and reconnected to ${targetDevice.deviceName || targetDevice.title}`
|
|
424
481
|
};
|
|
425
482
|
}
|
|
426
483
|
else {
|
|
@@ -474,19 +531,184 @@ function formatTreeStructureOnly(node, indent = 0) {
|
|
|
474
531
|
}
|
|
475
532
|
return result;
|
|
476
533
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Classify each root as "screen" or "overlay".
|
|
536
|
+
*
|
|
537
|
+
* A root is a **screen** if its subtree contains a navigation screen marker
|
|
538
|
+
* (Route(...), *Screen, *Page). Falls back to the largest root by area
|
|
539
|
+
* if no navigation markers are found. Everything else is an overlay.
|
|
540
|
+
*/
|
|
541
|
+
function classifyRoots(roots, elements, childrenMap) {
|
|
542
|
+
const labels = [];
|
|
543
|
+
// Navigation screen markers: Route(...) wrapper or user-defined *Screen/*Page
|
|
544
|
+
const screenMarker = /^(Route\(|.*Screen\(|.*Page\(|.*Screen$|.*Page$)/;
|
|
545
|
+
function subtreeHasScreen(idx, depth) {
|
|
546
|
+
if (depth > 30)
|
|
547
|
+
return false;
|
|
548
|
+
if (screenMarker.test(elements[idx].component))
|
|
549
|
+
return true;
|
|
550
|
+
const kids = childrenMap.get(idx);
|
|
551
|
+
if (kids) {
|
|
552
|
+
for (const kid of kids) {
|
|
553
|
+
if (subtreeHasScreen(kid, depth + 1))
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
// First pass: check which roots contain a navigation screen
|
|
560
|
+
const hasScreen = roots.map(ri => subtreeHasScreen(ri, 0));
|
|
561
|
+
const anyScreenFound = hasScreen.some(Boolean);
|
|
562
|
+
if (anyScreenFound) {
|
|
563
|
+
for (let i = 0; i < roots.length; i++) {
|
|
564
|
+
labels.push(hasScreen[i] ? "screen" : "overlay");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Fallback: largest root by area is the screen
|
|
569
|
+
let maxArea = 0;
|
|
570
|
+
let maxIdx = 0;
|
|
571
|
+
for (let i = 0; i < roots.length; i++) {
|
|
572
|
+
const f = elements[roots[i]].frame;
|
|
573
|
+
if (f && f.width * f.height > maxArea) {
|
|
574
|
+
maxArea = f.width * f.height;
|
|
575
|
+
maxIdx = i;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (let i = 0; i < roots.length; i++) {
|
|
579
|
+
labels.push(i === maxIdx ? "screen" : "overlay");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { labels, hasOverlays: labels.some(l => l === "overlay") };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Build, collapse, classify, and render a layout tree.
|
|
586
|
+
*
|
|
587
|
+
* Shared by both get_screen_layout (points, no tap coords) and
|
|
588
|
+
* the screenshot layout enrichment (pixels, with tap coords).
|
|
589
|
+
*
|
|
590
|
+
* @param elements - flat list of layout nodes with parentIndex linkage
|
|
591
|
+
* @param renderLine - callback that produces the output line for a node,
|
|
592
|
+
* given (element, indent level, whether it's a leaf)
|
|
593
|
+
*/
|
|
594
|
+
function formatLayoutTree(elements, renderLine) {
|
|
595
|
+
// Build index: originalIndex -> element index in the filtered array
|
|
596
|
+
const indexMap = new Map();
|
|
597
|
+
for (let i = 0; i < elements.length; i++) {
|
|
598
|
+
if (elements[i].originalIndex !== undefined) {
|
|
599
|
+
indexMap.set(elements[i].originalIndex, i);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Build children lists
|
|
603
|
+
const children = new Map();
|
|
604
|
+
const roots = [];
|
|
605
|
+
for (let i = 0; i < elements.length; i++) {
|
|
606
|
+
const el = elements[i];
|
|
607
|
+
const parentOrigIdx = el.parentIndex;
|
|
608
|
+
if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
|
|
609
|
+
roots.push(i);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const parentIdx = indexMap.get(parentOrigIdx);
|
|
613
|
+
if (!children.has(parentIdx))
|
|
614
|
+
children.set(parentIdx, []);
|
|
615
|
+
children.get(parentIdx).push(i);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Collapse wrapper chains: if a child has the same frame as its parent,
|
|
619
|
+
// it's just a wrapper — promote its children and text to the parent
|
|
620
|
+
function sameFrame(a, b) {
|
|
621
|
+
if (!a.frame || !b.frame)
|
|
622
|
+
return false;
|
|
623
|
+
return Math.abs(a.frame.x - b.frame.x) < 1 &&
|
|
624
|
+
Math.abs(a.frame.y - b.frame.y) < 1 &&
|
|
625
|
+
Math.abs(a.frame.width - b.frame.width) < 1 &&
|
|
626
|
+
Math.abs(a.frame.height - b.frame.height) < 1;
|
|
627
|
+
}
|
|
628
|
+
function collapseNode(parentIdx) {
|
|
629
|
+
const kids = children.get(parentIdx);
|
|
630
|
+
if (!kids)
|
|
631
|
+
return;
|
|
632
|
+
const newKids = [];
|
|
633
|
+
for (const kidIdx of kids) {
|
|
634
|
+
if (sameFrame(elements[parentIdx], elements[kidIdx])) {
|
|
635
|
+
if (!elements[parentIdx].text && elements[kidIdx].text) {
|
|
636
|
+
elements[parentIdx].text = elements[kidIdx].text;
|
|
637
|
+
}
|
|
638
|
+
const grandKids = children.get(kidIdx);
|
|
639
|
+
if (grandKids) {
|
|
640
|
+
newKids.push(...grandKids);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
newKids.push(kidIdx);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (newKids.length > 0) {
|
|
648
|
+
children.set(parentIdx, newKids);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
children.delete(parentIdx);
|
|
652
|
+
}
|
|
653
|
+
const updatedKids = children.get(parentIdx);
|
|
654
|
+
if (updatedKids) {
|
|
655
|
+
for (const kid of updatedKids) {
|
|
656
|
+
collapseNode(kid);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const root of roots) {
|
|
661
|
+
collapseNode(root);
|
|
662
|
+
}
|
|
663
|
+
// Render tree
|
|
664
|
+
const lines = [];
|
|
665
|
+
function printNode(idx, indent) {
|
|
666
|
+
const isLeaf = !children.has(idx);
|
|
667
|
+
lines.push(renderLine(elements[idx], indent, isLeaf));
|
|
668
|
+
const kids = children.get(idx);
|
|
669
|
+
if (kids) {
|
|
670
|
+
for (const kid of kids) {
|
|
671
|
+
printNode(kid, indent + 1);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Classify roots and emit layer headers
|
|
676
|
+
const { labels, hasOverlays } = classifyRoots(roots, elements, children);
|
|
677
|
+
if (hasOverlays) {
|
|
678
|
+
let prevLabel = "";
|
|
679
|
+
for (let ri = 0; ri < roots.length; ri++) {
|
|
680
|
+
const label = labels[ri];
|
|
681
|
+
if (label === "overlay" || label !== prevLabel) {
|
|
682
|
+
if (lines.length > 0)
|
|
683
|
+
lines.push("");
|
|
684
|
+
lines.push(`[${label}]`);
|
|
685
|
+
}
|
|
686
|
+
prevLabel = label;
|
|
687
|
+
printNode(roots[ri], 0);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
for (const root of roots) {
|
|
692
|
+
printNode(root, 0);
|
|
693
|
+
}
|
|
487
694
|
}
|
|
488
695
|
return lines.join("\n");
|
|
489
696
|
}
|
|
697
|
+
function formatScreenLayoutTree(elements, extended = false) {
|
|
698
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
699
|
+
const prefix = " ".repeat(indent);
|
|
700
|
+
const frame = el.frame
|
|
701
|
+
? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
|
|
702
|
+
: "";
|
|
703
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
704
|
+
const idStr = id ? ` [${id}]` : "";
|
|
705
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
706
|
+
const layoutStr = extended && el.layout
|
|
707
|
+
? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
|
|
708
|
+
: "";
|
|
709
|
+
return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
490
712
|
function formatFoundComponentsToTonl(components) {
|
|
491
713
|
const lines = ["#found{component,path,depth,key,layout}"];
|
|
492
714
|
for (const c of components) {
|
|
@@ -511,11 +733,11 @@ function formatSummaryToTonl(components, total) {
|
|
|
511
733
|
* This traverses the fiber tree to extract component hierarchy with names.
|
|
512
734
|
*/
|
|
513
735
|
export async function getComponentTree(options = {}) {
|
|
514
|
-
const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false } = options;
|
|
736
|
+
const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false, device } = options;
|
|
515
737
|
// Use lower default depth for structureOnly to keep output compact (~2-5KB)
|
|
516
738
|
// Full mode uses higher depth since TONL format handles it better
|
|
517
739
|
// focusedOnly mode uses moderate depth since we're already filtering to active screen
|
|
518
|
-
const maxDepth = options.maxDepth ??
|
|
740
|
+
const maxDepth = options.maxDepth ?? 5000;
|
|
519
741
|
const expression = `
|
|
520
742
|
(function() {
|
|
521
743
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -660,7 +882,7 @@ export async function getComponentTree(options = {}) {
|
|
|
660
882
|
|
|
661
883
|
// Find focused screen if requested
|
|
662
884
|
function findFocusedScreen(fiber, depth = 0) {
|
|
663
|
-
if (!fiber || depth >
|
|
885
|
+
if (!fiber || depth > 5000) return null;
|
|
664
886
|
|
|
665
887
|
const name = getComponentName(fiber);
|
|
666
888
|
|
|
@@ -710,7 +932,7 @@ export async function getComponentTree(options = {}) {
|
|
|
710
932
|
})()
|
|
711
933
|
`;
|
|
712
934
|
// Use a longer timeout for component tree traversal — large apps can exceed 10s
|
|
713
|
-
const result = await executeInApp(expression, false, { timeoutMs: 30000 });
|
|
935
|
+
const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
|
|
714
936
|
// Apply formatting if requested
|
|
715
937
|
if (result.success && result.result) {
|
|
716
938
|
try {
|
|
@@ -736,33 +958,57 @@ export async function getComponentTree(options = {}) {
|
|
|
736
958
|
return result;
|
|
737
959
|
}
|
|
738
960
|
/**
|
|
739
|
-
* Get layout
|
|
740
|
-
*
|
|
961
|
+
* Get layout data for visible components on the current screen.
|
|
962
|
+
* Uses measureInWindow to get actual screen positions and filters
|
|
963
|
+
* to only components within the viewport.
|
|
964
|
+
*
|
|
965
|
+
* Two-step approach (same as inspectAtPoint):
|
|
966
|
+
* Step 1: Walk fiber tree, dispatch measureInWindow on host components
|
|
967
|
+
* Step 2: After 300ms, read measurements, filter by viewport, build results
|
|
741
968
|
*/
|
|
742
969
|
export async function getScreenLayout(options = {}) {
|
|
743
|
-
const {
|
|
744
|
-
const
|
|
970
|
+
const { extended = false, summary = false, device, raw = false } = options;
|
|
971
|
+
const maxDepth = 5000;
|
|
972
|
+
const componentsOnly = true;
|
|
973
|
+
const shortPath = true;
|
|
974
|
+
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
975
|
+
const dispatchExpression = `
|
|
745
976
|
(function() {
|
|
746
|
-
|
|
977
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
747
978
|
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
748
979
|
|
|
749
|
-
|
|
980
|
+
var roots = [];
|
|
750
981
|
if (hook.getFiberRoots) {
|
|
751
|
-
roots =
|
|
982
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
752
983
|
}
|
|
753
984
|
if (roots.length === 0 && hook.renderers) {
|
|
754
|
-
for (
|
|
755
|
-
|
|
985
|
+
for (var entry of hook.renderers) {
|
|
986
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
756
987
|
if (r.length > 0) { roots = r; break; }
|
|
757
988
|
}
|
|
758
989
|
}
|
|
759
990
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
760
991
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
992
|
+
function getMeasurable(fiber) {
|
|
993
|
+
var sn = fiber.stateNode;
|
|
994
|
+
if (!sn) return null;
|
|
995
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
996
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
997
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
998
|
+
return sn.canonical.publicInstance;
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Collect host fibers with their metadata
|
|
1004
|
+
var hostFibers = [];
|
|
1005
|
+
var fiberMeta = [];
|
|
1006
|
+
var componentsOnlyMode = ${componentsOnly};
|
|
1007
|
+
|
|
1008
|
+
// RN internals and primitives — skip these when looking for meaningful component names
|
|
1009
|
+
// Only filter internal components that can't be written as JSX.
|
|
1010
|
+
// Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
|
|
1011
|
+
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)$/;
|
|
766
1012
|
|
|
767
1013
|
function getComponentName(fiber) {
|
|
768
1014
|
if (!fiber || !fiber.type) return null;
|
|
@@ -770,8 +1016,212 @@ export async function getScreenLayout(options = {}) {
|
|
|
770
1016
|
return fiber.type.displayName || fiber.type.name || null;
|
|
771
1017
|
}
|
|
772
1018
|
|
|
773
|
-
|
|
774
|
-
|
|
1019
|
+
// Find the first measurable host descendant of a fiber
|
|
1020
|
+
function findFirstHost(fiber, depth) {
|
|
1021
|
+
if (!fiber || depth > 20) return null;
|
|
1022
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
|
|
1023
|
+
var child = fiber.child;
|
|
1024
|
+
while (child) {
|
|
1025
|
+
var found = findFirstHost(child, depth + 1);
|
|
1026
|
+
if (found) return found;
|
|
1027
|
+
child = child.sibling;
|
|
1028
|
+
}
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Extract text content from a fiber subtree.
|
|
1033
|
+
// When a fiber has a string child, return it without recursing
|
|
1034
|
+
// (avoids duplication from Text > RCTText having the same string).
|
|
1035
|
+
function collectText(fiber, d) {
|
|
1036
|
+
if (!fiber || d > 30) return '';
|
|
1037
|
+
var props = fiber.memoizedProps;
|
|
1038
|
+
if (props) {
|
|
1039
|
+
var ch = props.children;
|
|
1040
|
+
// Leaf text — return without recursing into children fibers
|
|
1041
|
+
if (typeof ch === 'string') return ch;
|
|
1042
|
+
if (typeof ch === 'number') return String(ch);
|
|
1043
|
+
if (Array.isArray(ch)) {
|
|
1044
|
+
var inline = [];
|
|
1045
|
+
for (var ci = 0; ci < ch.length; ci++) {
|
|
1046
|
+
if (typeof ch[ci] === 'string') inline.push(ch[ci]);
|
|
1047
|
+
else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
|
|
1048
|
+
}
|
|
1049
|
+
if (inline.length > 0) return inline.join('');
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// No direct text — collect from child fibers (siblings = adjacent elements)
|
|
1053
|
+
var parts = [];
|
|
1054
|
+
var child = fiber.child;
|
|
1055
|
+
while (child) {
|
|
1056
|
+
var t = collectText(child, d + 1);
|
|
1057
|
+
if (t) parts.push(t);
|
|
1058
|
+
child = child.sibling;
|
|
1059
|
+
}
|
|
1060
|
+
return parts.join(' ').trim();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (componentsOnlyMode) {
|
|
1064
|
+
// componentsOnly: walk tree looking for meaningful custom components,
|
|
1065
|
+
// measure their first host child, track parent for tree output
|
|
1066
|
+
function walkComponents(fiber, depth, path, parentIdx) {
|
|
1067
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1068
|
+
var name = getComponentName(fiber);
|
|
1069
|
+
var isHost = typeof fiber.type === 'string';
|
|
1070
|
+
|
|
1071
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1072
|
+
// active: 0 = inactive/detached, 1 = transitioning, 2 = active
|
|
1073
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1074
|
+
|
|
1075
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1076
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1077
|
+
|
|
1078
|
+
var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
|
|
1079
|
+
|
|
1080
|
+
var myIdx = parentIdx;
|
|
1081
|
+
if (isMeaningful) {
|
|
1082
|
+
var host = findFirstHost(fiber, 0);
|
|
1083
|
+
if (host) {
|
|
1084
|
+
myIdx = hostFibers.length;
|
|
1085
|
+
// Extract text from this component's subtree
|
|
1086
|
+
var text = collectText(fiber, 0);
|
|
1087
|
+
hostFibers.push(host);
|
|
1088
|
+
fiberMeta.push({
|
|
1089
|
+
hostName: typeof host.type === 'string' ? host.type : '',
|
|
1090
|
+
customName: name,
|
|
1091
|
+
depth: depth,
|
|
1092
|
+
path: path.concat([name]),
|
|
1093
|
+
parentIndex: parentIdx,
|
|
1094
|
+
text: text ? text.slice(0, 80) : null
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
var child = fiber.child;
|
|
1100
|
+
while (child) {
|
|
1101
|
+
var childName = getComponentName(child);
|
|
1102
|
+
walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
|
|
1103
|
+
child = child.sibling;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
walkComponents(roots[0].current, 0, [], -1);
|
|
1107
|
+
} else {
|
|
1108
|
+
// Default mode: collect all host fibers with ancestor info
|
|
1109
|
+
function walkFibers(fiber, depth, path) {
|
|
1110
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1111
|
+
var name = getComponentName(fiber);
|
|
1112
|
+
var isHost = typeof fiber.type === 'string';
|
|
1113
|
+
|
|
1114
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1115
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1116
|
+
|
|
1117
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1118
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1119
|
+
|
|
1120
|
+
if (name && isHost && getMeasurable(fiber)) {
|
|
1121
|
+
// Find nearest meaningful custom component ancestor for display
|
|
1122
|
+
var customName = null;
|
|
1123
|
+
var fallbackName = null;
|
|
1124
|
+
var cur = fiber.return;
|
|
1125
|
+
while (cur) {
|
|
1126
|
+
if (cur.type && typeof cur.type !== 'string') {
|
|
1127
|
+
var cName = cur.type.displayName || cur.type.name || null;
|
|
1128
|
+
if (cName) {
|
|
1129
|
+
if (!fallbackName) fallbackName = cName;
|
|
1130
|
+
if (!RN_PRIMITIVES.test(cName)) {
|
|
1131
|
+
customName = cName;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
cur = cur.return;
|
|
1137
|
+
}
|
|
1138
|
+
if (!customName) customName = fallbackName;
|
|
1139
|
+
|
|
1140
|
+
hostFibers.push(fiber);
|
|
1141
|
+
fiberMeta.push({
|
|
1142
|
+
hostName: name,
|
|
1143
|
+
customName: customName,
|
|
1144
|
+
depth: depth,
|
|
1145
|
+
path: path.slice()
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
var child = fiber.child;
|
|
1150
|
+
while (child) {
|
|
1151
|
+
var childName = getComponentName(child);
|
|
1152
|
+
walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1153
|
+
child = child.sibling;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
walkFibers(roots[0].current, 0, []);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
|
|
1160
|
+
|
|
1161
|
+
// Store fibers and metadata globally for step 2
|
|
1162
|
+
globalThis.__layoutFibers = hostFibers;
|
|
1163
|
+
globalThis.__layoutMeta = fiberMeta;
|
|
1164
|
+
globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
|
|
1165
|
+
|
|
1166
|
+
// Dispatch measureInWindow on all host fibers
|
|
1167
|
+
for (var i = 0; i < hostFibers.length; i++) {
|
|
1168
|
+
try {
|
|
1169
|
+
(function(idx) {
|
|
1170
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
1171
|
+
globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
1172
|
+
});
|
|
1173
|
+
})(i);
|
|
1174
|
+
} catch(e) {}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return { count: hostFibers.length };
|
|
1178
|
+
})()
|
|
1179
|
+
`;
|
|
1180
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
|
|
1181
|
+
if (!dispatchResult.success)
|
|
1182
|
+
return dispatchResult;
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
1185
|
+
if (parsed.error)
|
|
1186
|
+
return { success: false, error: parsed.error };
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
/* ignore */
|
|
1190
|
+
}
|
|
1191
|
+
// Wait for measureInWindow callbacks
|
|
1192
|
+
await delay(300);
|
|
1193
|
+
// --- Step 2: read measurements, filter visible, build results ---
|
|
1194
|
+
const resolveExpression = `
|
|
1195
|
+
(function() {
|
|
1196
|
+
var fibers = globalThis.__layoutFibers;
|
|
1197
|
+
var meta = globalThis.__layoutMeta;
|
|
1198
|
+
var measurements = globalThis.__layoutMeasurements;
|
|
1199
|
+
globalThis.__layoutFibers = null;
|
|
1200
|
+
globalThis.__layoutMeta = null;
|
|
1201
|
+
globalThis.__layoutMeasurements = null;
|
|
1202
|
+
|
|
1203
|
+
if (!fibers || !measurements || !meta) {
|
|
1204
|
+
return { error: 'No measurement data. Run get_screen_layout again.' };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
var componentsOnly = ${componentsOnly};
|
|
1208
|
+
var shortPath = ${shortPath};
|
|
1209
|
+
var summaryMode = ${summary};
|
|
1210
|
+
var pathSegments = 3;
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
// Get viewport dimensions from the first root view measurement
|
|
1214
|
+
// Accept elements starting at x=0 even with negative y (safe area extensions)
|
|
1215
|
+
var viewportW = 9999, viewportH = 9999;
|
|
1216
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
1217
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
|
|
1218
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
1219
|
+
viewportW = measurements[v].width;
|
|
1220
|
+
// For wrappers extending behind safe area, the visible viewport height
|
|
1221
|
+
// is the total height minus the negative offset
|
|
1222
|
+
viewportH = measurements[v].height + measurements[v].y;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
775
1225
|
}
|
|
776
1226
|
|
|
777
1227
|
function formatPath(pathArray) {
|
|
@@ -781,22 +1231,14 @@ export async function getScreenLayout(options = {}) {
|
|
|
781
1231
|
return '... > ' + pathArray.slice(-pathSegments).join(' > ');
|
|
782
1232
|
}
|
|
783
1233
|
|
|
784
|
-
function extractAllStyles(style) {
|
|
785
|
-
if (!style) return null;
|
|
786
|
-
const merged = Array.isArray(style)
|
|
787
|
-
? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
|
|
788
|
-
: (typeof style === 'object' ? style : {});
|
|
789
|
-
return Object.keys(merged).length > 0 ? merged : null;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
1234
|
function extractLayoutStyles(style) {
|
|
793
1235
|
if (!style) return null;
|
|
794
|
-
|
|
795
|
-
? Object.assign({}
|
|
1236
|
+
var merged = Array.isArray(style)
|
|
1237
|
+
? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
|
|
796
1238
|
: (typeof style === 'object' ? style : {});
|
|
797
1239
|
|
|
798
|
-
|
|
799
|
-
|
|
1240
|
+
var layout = {};
|
|
1241
|
+
var layoutKeys = [
|
|
800
1242
|
'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
801
1243
|
'paddingHorizontal', 'paddingVertical',
|
|
802
1244
|
'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
@@ -807,79 +1249,109 @@ export async function getScreenLayout(options = {}) {
|
|
|
807
1249
|
'position', 'top', 'bottom', 'left', 'right',
|
|
808
1250
|
'gap', 'rowGap', 'columnGap',
|
|
809
1251
|
'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
|
|
810
|
-
'backgroundColor', 'borderColor', 'borderRadius'
|
|
1252
|
+
'backgroundColor', 'borderColor', 'borderRadius',
|
|
1253
|
+
'zIndex', 'elevation'
|
|
811
1254
|
];
|
|
812
1255
|
|
|
813
|
-
for (
|
|
814
|
-
if (merged[
|
|
1256
|
+
for (var k = 0; k < layoutKeys.length; k++) {
|
|
1257
|
+
if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
|
|
815
1258
|
}
|
|
816
1259
|
return Object.keys(layout).length > 0 ? layout : null;
|
|
817
1260
|
}
|
|
818
1261
|
|
|
819
|
-
|
|
1262
|
+
var elements = [];
|
|
820
1263
|
|
|
821
|
-
|
|
822
|
-
|
|
1264
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
1265
|
+
var m = measurements[i];
|
|
1266
|
+
if (!m) continue;
|
|
823
1267
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (name && (!componentsOnly || !isHost)) {
|
|
829
|
-
const style = fiber.memoizedProps?.style;
|
|
830
|
-
const layout = extractLayoutStyles(style);
|
|
831
|
-
|
|
832
|
-
// Get text content if it's a Text component
|
|
833
|
-
let textContent = null;
|
|
834
|
-
if (name === 'Text' || name === 'RCTText') {
|
|
835
|
-
const children = fiber.memoizedProps?.children;
|
|
836
|
-
if (typeof children === 'string') textContent = children;
|
|
837
|
-
else if (typeof children === 'number') textContent = String(children);
|
|
838
|
-
}
|
|
1268
|
+
// Filter: only visible within viewport (with some tolerance for partial visibility)
|
|
1269
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
1270
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
1271
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
839
1272
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
path: formatPath(path),
|
|
843
|
-
depth
|
|
844
|
-
};
|
|
1273
|
+
var fiber = fibers[i];
|
|
1274
|
+
var info = meta[i];
|
|
845
1275
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
// Include key props for identification
|
|
850
|
-
if (fiber.memoizedProps) {
|
|
851
|
-
const identifiers = {};
|
|
852
|
-
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
853
|
-
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
854
|
-
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
855
|
-
if (fiber.key) identifiers.key = fiber.key;
|
|
856
|
-
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
857
|
-
}
|
|
1276
|
+
var displayName = componentsOnly ? info.customName : info.hostName;
|
|
1277
|
+
if (!displayName) continue;
|
|
1278
|
+
if (componentsOnly && !info.customName) continue;
|
|
858
1279
|
|
|
859
|
-
|
|
1280
|
+
var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
|
|
1281
|
+
var layout = extractLayoutStyles(style);
|
|
1282
|
+
|
|
1283
|
+
// Get text content — use pre-collected text from step 1, or fall back to host fiber
|
|
1284
|
+
var textContent = info.text || null;
|
|
1285
|
+
if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
|
|
1286
|
+
var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
|
|
1287
|
+
if (typeof children === 'string') textContent = children;
|
|
1288
|
+
else if (typeof children === 'number') textContent = String(children);
|
|
860
1289
|
}
|
|
861
1290
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1291
|
+
var element = {
|
|
1292
|
+
component: displayName,
|
|
1293
|
+
path: formatPath(info.path),
|
|
1294
|
+
depth: info.depth,
|
|
1295
|
+
frame: { x: m.x, y: m.y, width: m.width, height: m.height },
|
|
1296
|
+
originalIndex: i
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
|
|
1300
|
+
if (layout) element.layout = layout;
|
|
1301
|
+
if (textContent) element.text = textContent.slice(0, 100);
|
|
1302
|
+
|
|
1303
|
+
// Identifiers
|
|
1304
|
+
if (fiber.memoizedProps) {
|
|
1305
|
+
var identifiers = {};
|
|
1306
|
+
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
1307
|
+
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
1308
|
+
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
1309
|
+
if (fiber.key) identifiers.key = fiber.key;
|
|
1310
|
+
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
elements.push(element);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// In componentsOnly mode, remove full-screen wrapper components
|
|
1317
|
+
// and re-parent their children to collapse the wrapper chain
|
|
1318
|
+
if (componentsOnly && elements.length > 0) {
|
|
1319
|
+
var filtered = [];
|
|
1320
|
+
// Map: originalIndex -> resolved parent for skipped wrappers
|
|
1321
|
+
var reparent = {};
|
|
1322
|
+
|
|
1323
|
+
for (var fi = 0; fi < elements.length; fi++) {
|
|
1324
|
+
var el = elements[fi];
|
|
1325
|
+
var fr = el.frame;
|
|
1326
|
+
// A wrapper is full-screen if it covers the entire viewport or extends beyond it
|
|
1327
|
+
// (e.g., y=-119 wrappers that extend behind the safe area)
|
|
1328
|
+
var isFullScreen = fr && fr.x <= 0 && fr.y <= 0 &&
|
|
1329
|
+
(fr.width >= viewportW - 2) && (fr.y + fr.height >= viewportH - 2);
|
|
1330
|
+
|
|
1331
|
+
if (isFullScreen) {
|
|
1332
|
+
// Skip this wrapper, map its originalIndex to its parent
|
|
1333
|
+
reparent[el.originalIndex] = el.parentIndex;
|
|
1334
|
+
} else {
|
|
1335
|
+
// Resolve parent through any skipped wrappers
|
|
1336
|
+
var resolvedParent = el.parentIndex;
|
|
1337
|
+
while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
|
|
1338
|
+
resolvedParent = reparent[resolvedParent];
|
|
1339
|
+
}
|
|
1340
|
+
el.parentIndex = resolvedParent;
|
|
1341
|
+
filtered.push(el);
|
|
1342
|
+
}
|
|
868
1343
|
}
|
|
1344
|
+
elements = filtered;
|
|
869
1345
|
}
|
|
870
1346
|
|
|
871
|
-
walkFiber(roots[0].current, 0, []);
|
|
872
|
-
|
|
873
|
-
// Summary mode: return counts by component name
|
|
874
1347
|
if (summaryMode) {
|
|
875
|
-
|
|
876
|
-
for (
|
|
877
|
-
counts[
|
|
1348
|
+
var counts = {};
|
|
1349
|
+
for (var j = 0; j < elements.length; j++) {
|
|
1350
|
+
counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
|
|
878
1351
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
.map(([name, count]) => ({ component: name, count }));
|
|
1352
|
+
var sorted = Object.keys(counts).map(function(name) {
|
|
1353
|
+
return { component: name, count: counts[name] };
|
|
1354
|
+
}).sort(function(a, b) { return b.count - a.count; });
|
|
883
1355
|
return {
|
|
884
1356
|
totalElements: elements.length,
|
|
885
1357
|
uniqueComponents: sorted.length,
|
|
@@ -888,15 +1360,15 @@ export async function getScreenLayout(options = {}) {
|
|
|
888
1360
|
}
|
|
889
1361
|
|
|
890
1362
|
return {
|
|
1363
|
+
viewport: { width: viewportW, height: viewportH },
|
|
891
1364
|
totalElements: elements.length,
|
|
892
1365
|
elements: elements
|
|
893
1366
|
};
|
|
894
1367
|
})()
|
|
895
1368
|
`;
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (format === "tonl" && result.success && result.result) {
|
|
1369
|
+
const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
|
|
1370
|
+
// Format output as tree
|
|
1371
|
+
if (result.success && result.result) {
|
|
900
1372
|
try {
|
|
901
1373
|
const parsed = JSON.parse(result.result);
|
|
902
1374
|
if (parsed.components) {
|
|
@@ -905,9 +1377,16 @@ export async function getScreenLayout(options = {}) {
|
|
|
905
1377
|
return { success: true, result: tonl };
|
|
906
1378
|
}
|
|
907
1379
|
else if (parsed.elements) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1380
|
+
if (raw) {
|
|
1381
|
+
return {
|
|
1382
|
+
success: true,
|
|
1383
|
+
result: result.result,
|
|
1384
|
+
parsedElements: parsed.elements,
|
|
1385
|
+
viewport: parsed.viewport
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const tree = formatScreenLayoutTree(parsed.elements, extended);
|
|
1389
|
+
return { success: true, result: tree };
|
|
911
1390
|
}
|
|
912
1391
|
}
|
|
913
1392
|
catch {
|
|
@@ -916,11 +1395,76 @@ export async function getScreenLayout(options = {}) {
|
|
|
916
1395
|
}
|
|
917
1396
|
return result;
|
|
918
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Format enriched elements as an indented tree with tap coordinates in pixels.
|
|
1400
|
+
* Same tree structure as get_screen_layout but with tap:(x,y) per node.
|
|
1401
|
+
*/
|
|
1402
|
+
function formatEnrichedLayoutTree(elements) {
|
|
1403
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
1404
|
+
const prefix = " ".repeat(indent);
|
|
1405
|
+
const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
|
|
1406
|
+
const tap = ` tap:(${el.tapX},${el.tapY})`;
|
|
1407
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
1408
|
+
const idStr = id ? ` [${id}]` : "";
|
|
1409
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
1410
|
+
return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
|
|
1415
|
+
* Converts points/dp frame coordinates to pixels using the device pixel ratio,
|
|
1416
|
+
* computes center-point tapX/tapY for each element.
|
|
1417
|
+
*
|
|
1418
|
+
* @param pixelRatio - device pixel ratio (e.g., 3 for @3x iPhone)
|
|
1419
|
+
* @param screenshotScaleFactor - if the screenshot image was scaled down, this factor adjusts coordinates
|
|
1420
|
+
* @param device - optional target device name
|
|
1421
|
+
* @returns formatted tree string with pixel coordinates, or null if unavailable
|
|
1422
|
+
*/
|
|
1423
|
+
export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device) {
|
|
1424
|
+
try {
|
|
1425
|
+
const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
|
|
1426
|
+
if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
|
|
1427
|
+
return null;
|
|
1428
|
+
const elements = result.parsedElements.map((el) => {
|
|
1429
|
+
const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
|
|
1430
|
+
// Convert center point from points/dp to screenshot pixels
|
|
1431
|
+
// Points -> device pixels: multiply by pixelRatio
|
|
1432
|
+
// Device pixels -> screenshot pixels: divide by screenshotScaleFactor (if image was resized)
|
|
1433
|
+
const centerXPoints = frame.x + frame.width / 2;
|
|
1434
|
+
const centerYPoints = frame.y + frame.height / 2;
|
|
1435
|
+
const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
|
|
1436
|
+
const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
|
|
1437
|
+
// Convert frame to pixels too
|
|
1438
|
+
const pixelFrame = {
|
|
1439
|
+
x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
|
|
1440
|
+
y: Math.round((frame.y * pixelRatio) / screenshotScaleFactor),
|
|
1441
|
+
width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
|
|
1442
|
+
height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
|
|
1443
|
+
};
|
|
1444
|
+
return {
|
|
1445
|
+
component: el.component,
|
|
1446
|
+
frame: pixelFrame,
|
|
1447
|
+
tapX,
|
|
1448
|
+
tapY,
|
|
1449
|
+
text: el.text,
|
|
1450
|
+
identifiers: el.identifiers,
|
|
1451
|
+
parentIndex: el.parentIndex,
|
|
1452
|
+
originalIndex: el.originalIndex,
|
|
1453
|
+
depth: el.depth,
|
|
1454
|
+
path: el.path,
|
|
1455
|
+
};
|
|
1456
|
+
});
|
|
1457
|
+
return formatEnrichedLayoutTree(elements);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
return null; // Non-fatal: screenshot works without layout
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
919
1463
|
/**
|
|
920
1464
|
* Inspect a specific component by name, returning its props, state, and layout.
|
|
921
1465
|
*/
|
|
922
1466
|
export async function inspectComponent(componentName, options = {}) {
|
|
923
|
-
const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true } = options;
|
|
1467
|
+
const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true, device } = options;
|
|
924
1468
|
const escapedName = componentName.replace(/'/g, "\\'");
|
|
925
1469
|
const expression = `
|
|
926
1470
|
(function() {
|
|
@@ -1126,13 +1670,13 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1126
1670
|
return result;
|
|
1127
1671
|
})()
|
|
1128
1672
|
`;
|
|
1129
|
-
return executeInApp(expression, false);
|
|
1673
|
+
return executeInApp(expression, false, {}, device);
|
|
1130
1674
|
}
|
|
1131
1675
|
/**
|
|
1132
1676
|
* Find all components matching a name pattern and return summary info.
|
|
1133
1677
|
*/
|
|
1134
1678
|
export async function findComponents(pattern, options = {}) {
|
|
1135
|
-
const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl" } = options;
|
|
1679
|
+
const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl", device } = options;
|
|
1136
1680
|
const escapedPattern = pattern.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
|
|
1137
1681
|
const expression = `
|
|
1138
1682
|
(function() {
|
|
@@ -1194,36 +1738,47 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1194
1738
|
function search(fiber, path, depth) {
|
|
1195
1739
|
if (!fiber || results.length >= maxResults) return;
|
|
1196
1740
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1741
|
+
try {
|
|
1742
|
+
var name = getComponentName(fiber);
|
|
1743
|
+
if (name && regex.test(name)) {
|
|
1744
|
+
var entry = {
|
|
1745
|
+
component: name,
|
|
1746
|
+
path: formatPath(path),
|
|
1747
|
+
depth
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
if (fiber.memoizedProps && fiber.memoizedProps.testID) entry.testID = fiber.memoizedProps.testID;
|
|
1751
|
+
if (fiber.key) entry.key = fiber.key;
|
|
1752
|
+
|
|
1753
|
+
if (includeLayout && fiber.memoizedProps && fiber.memoizedProps.style) {
|
|
1754
|
+
try {
|
|
1755
|
+
var layout = extractLayoutStyles(fiber.memoizedProps.style);
|
|
1756
|
+
if (layout) entry.layout = layout;
|
|
1757
|
+
} catch(e) {}
|
|
1758
|
+
}
|
|
1207
1759
|
|
|
1208
|
-
|
|
1209
|
-
const layout = extractLayoutStyles(fiber.memoizedProps.style);
|
|
1210
|
-
if (layout) entry.layout = layout;
|
|
1760
|
+
results.push(entry);
|
|
1211
1761
|
}
|
|
1212
1762
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1763
|
+
var child = fiber.child;
|
|
1764
|
+
while (child && results.length < maxResults) {
|
|
1765
|
+
var childName = getComponentName(child);
|
|
1766
|
+
search(child, childName ? path.concat([childName]) : path, depth + 1);
|
|
1767
|
+
child = child.sibling;
|
|
1768
|
+
}
|
|
1769
|
+
} catch(e) {
|
|
1770
|
+
try {
|
|
1771
|
+
var child = fiber.child;
|
|
1772
|
+
while (child && results.length < maxResults) {
|
|
1773
|
+
search(child, path, depth + 1);
|
|
1774
|
+
child = child.sibling;
|
|
1775
|
+
}
|
|
1776
|
+
} catch(e2) {}
|
|
1221
1777
|
}
|
|
1222
1778
|
}
|
|
1223
1779
|
|
|
1224
1780
|
search(roots[0].current, [], 0);
|
|
1225
1781
|
|
|
1226
|
-
// Summary mode: just return counts by component name
|
|
1227
1782
|
if (summaryMode) {
|
|
1228
1783
|
const counts = {};
|
|
1229
1784
|
for (const r of results) {
|
|
@@ -1247,19 +1802,16 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1247
1802
|
};
|
|
1248
1803
|
})()
|
|
1249
1804
|
`;
|
|
1250
|
-
const result = await executeInApp(expression, false);
|
|
1251
|
-
// Apply TONL formatting if requested
|
|
1805
|
+
const result = await executeInApp(expression, false, {}, device);
|
|
1252
1806
|
if (format === "tonl" && result.success && result.result) {
|
|
1253
1807
|
try {
|
|
1254
1808
|
const parsed = JSON.parse(result.result);
|
|
1255
1809
|
if (parsed.components) {
|
|
1256
1810
|
if (parsed.totalMatches !== undefined) {
|
|
1257
|
-
// Summary mode
|
|
1258
1811
|
const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
|
|
1259
1812
|
return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
|
|
1260
1813
|
}
|
|
1261
1814
|
else {
|
|
1262
|
-
// Full list mode
|
|
1263
1815
|
const tonl = formatFoundComponentsToTonl(parsed.components);
|
|
1264
1816
|
return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
|
|
1265
1817
|
}
|
|
@@ -1316,7 +1868,7 @@ export async function pressElement(options) {
|
|
|
1316
1868
|
}
|
|
1317
1869
|
|
|
1318
1870
|
function extractText(fiber, depth) {
|
|
1319
|
-
if (!fiber || depth >
|
|
1871
|
+
if (!fiber || depth > 5000) return '';
|
|
1320
1872
|
var parts = [];
|
|
1321
1873
|
var props = fiber.memoizedProps;
|
|
1322
1874
|
if (props) {
|
|
@@ -1399,7 +1951,95 @@ export async function pressElement(options) {
|
|
|
1399
1951
|
walkFiber(roots[ri].current, []);
|
|
1400
1952
|
}
|
|
1401
1953
|
|
|
1402
|
-
// Phase
|
|
1954
|
+
// Phase 2a: If searching by testID and no match found,
|
|
1955
|
+
// the testID may be on a non-pressable/non-input wrapper — search ALL nodes
|
|
1956
|
+
// for the testID and walk UP to find nearest pressable or input ancestor
|
|
1957
|
+
if (matches.length === 0 && searchTestID !== null) {
|
|
1958
|
+
var testIDCandidates = [];
|
|
1959
|
+
|
|
1960
|
+
function findByTestID(fiber, path) {
|
|
1961
|
+
if (!fiber) return;
|
|
1962
|
+
var name = getComponentName(fiber);
|
|
1963
|
+
var props = fiber.memoizedProps;
|
|
1964
|
+
if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
|
|
1965
|
+
|
|
1966
|
+
var tid = props && (props.testID || props.nativeID || null);
|
|
1967
|
+
if (tid === searchTestID) {
|
|
1968
|
+
testIDCandidates.push({ fiber: fiber, name: name || '(anonymous)', path: path.join(' > ') });
|
|
1969
|
+
}
|
|
1970
|
+
var child = fiber.child;
|
|
1971
|
+
while (child) {
|
|
1972
|
+
var childName = getComponentName(child);
|
|
1973
|
+
findByTestID(child, childName ? path.concat([childName]) : path);
|
|
1974
|
+
child = child.sibling;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
for (var ri3 = 0; ri3 < roots.length; ri3++) {
|
|
1979
|
+
findByTestID(roots[ri3].current, []);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// For each candidate, first check if the node itself is pressable/input,
|
|
1983
|
+
// then walk UP to find nearest pressable/input ancestor
|
|
1984
|
+
var maxDepthTID = ${maxTraversalDepth};
|
|
1985
|
+
for (var ti = 0; ti < testIDCandidates.length; ti++) {
|
|
1986
|
+
var tCandidate = testIDCandidates[ti];
|
|
1987
|
+
var tProps = tCandidate.fiber.memoizedProps;
|
|
1988
|
+
var tIsPressable = tProps && typeof tProps.onPress === 'function';
|
|
1989
|
+
var tIsInput = !tIsPressable && tProps && (typeof tProps.onChangeText === 'function' || typeof tProps.onFocus === 'function');
|
|
1990
|
+
|
|
1991
|
+
if (tIsPressable || tIsInput) {
|
|
1992
|
+
var text = extractText(tCandidate.fiber, 0);
|
|
1993
|
+
if (tIsInput) {
|
|
1994
|
+
var val = typeof tProps.value === 'string' ? tProps.value : '';
|
|
1995
|
+
var defVal = typeof tProps.defaultValue === 'string' ? tProps.defaultValue : '';
|
|
1996
|
+
var ph = typeof tProps.placeholder === 'string' ? tProps.placeholder : '';
|
|
1997
|
+
text = text || val || defVal || ph;
|
|
1998
|
+
}
|
|
1999
|
+
matches.push({
|
|
2000
|
+
fiber: tCandidate.fiber,
|
|
2001
|
+
name: tCandidate.name,
|
|
2002
|
+
text: text.substring(0, 100),
|
|
2003
|
+
testID: searchTestID,
|
|
2004
|
+
path: tCandidate.path,
|
|
2005
|
+
isInput: tIsInput
|
|
2006
|
+
});
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// Walk up to find pressable/input ancestor
|
|
2011
|
+
var tParent = tCandidate.fiber.return;
|
|
2012
|
+
var tDepth = 0;
|
|
2013
|
+
while (tParent && tDepth < maxDepthTID) {
|
|
2014
|
+
var tParentProps = tParent.memoizedProps;
|
|
2015
|
+
var parentIsPressable = tParentProps && typeof tParentProps.onPress === 'function';
|
|
2016
|
+
var parentIsInput = !parentIsPressable && tParentProps && (typeof tParentProps.onChangeText === 'function' || typeof tParentProps.onFocus === 'function');
|
|
2017
|
+
|
|
2018
|
+
if (parentIsPressable || parentIsInput) {
|
|
2019
|
+
var text = extractText(tParent, 0);
|
|
2020
|
+
if (parentIsInput) {
|
|
2021
|
+
var val = typeof tParentProps.value === 'string' ? tParentProps.value : '';
|
|
2022
|
+
var defVal = typeof tParentProps.defaultValue === 'string' ? tParentProps.defaultValue : '';
|
|
2023
|
+
var ph = typeof tParentProps.placeholder === 'string' ? tParentProps.placeholder : '';
|
|
2024
|
+
text = text || val || defVal || ph;
|
|
2025
|
+
}
|
|
2026
|
+
matches.push({
|
|
2027
|
+
fiber: tParent,
|
|
2028
|
+
name: tCandidate.name,
|
|
2029
|
+
text: text.substring(0, 100),
|
|
2030
|
+
testID: tParentProps.testID || tParentProps.nativeID || searchTestID,
|
|
2031
|
+
path: tCandidate.path,
|
|
2032
|
+
isInput: parentIsInput
|
|
2033
|
+
});
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
tParent = tParent.return;
|
|
2037
|
+
tDepth++;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// Phase 2b: If searching by component and no match found,
|
|
1403
2043
|
// look for the component by name and walk UP to find nearest pressable ancestor
|
|
1404
2044
|
if (matches.length === 0 && searchComponent !== null) {
|
|
1405
2045
|
var componentCandidates = [];
|
|
@@ -1469,49 +2109,136 @@ export async function pressElement(options) {
|
|
|
1469
2109
|
|
|
1470
2110
|
var target = matches[targetIndex];
|
|
1471
2111
|
try {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
pressed: target.name,
|
|
1478
|
-
matchIndex: targetIndex,
|
|
1479
|
-
totalMatches: matches.length,
|
|
1480
|
-
text: target.text,
|
|
1481
|
-
testID: target.testID,
|
|
1482
|
-
path: target.path,
|
|
1483
|
-
isInput: true,
|
|
1484
|
-
needsNativeTap: true
|
|
1485
|
-
};
|
|
1486
|
-
if (matches.length > 1) {
|
|
1487
|
-
result.allMatches = matches.map(function(m, i) {
|
|
1488
|
-
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
return result;
|
|
1492
|
-
}
|
|
1493
|
-
target.fiber.memoizedProps.onPress();
|
|
1494
|
-
var result = {
|
|
2112
|
+
// Both pressable and input elements use native tap via measured coordinates.
|
|
2113
|
+
// This ensures onPress wrappers, analytics, debouncing, and state tracking
|
|
2114
|
+
// logic inside components is executed through React's event pipeline,
|
|
2115
|
+
// rather than directly invoking the parent-supplied onPress prop.
|
|
2116
|
+
var baseResult = {
|
|
1495
2117
|
success: true,
|
|
1496
2118
|
pressed: target.name,
|
|
1497
2119
|
matchIndex: targetIndex,
|
|
1498
2120
|
totalMatches: matches.length,
|
|
1499
2121
|
text: target.text,
|
|
1500
2122
|
testID: target.testID,
|
|
1501
|
-
path: target.path
|
|
2123
|
+
path: target.path,
|
|
2124
|
+
isInput: !!target.isInput,
|
|
2125
|
+
needsNativeTap: true,
|
|
2126
|
+
nativeTapTarget: null
|
|
1502
2127
|
};
|
|
1503
2128
|
if (matches.length > 1) {
|
|
1504
|
-
|
|
2129
|
+
baseResult.allMatches = matches.map(function(m, i) {
|
|
1505
2130
|
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
1506
2131
|
});
|
|
1507
2132
|
}
|
|
1508
|
-
|
|
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;
|
|
1509
2236
|
} catch (e) {
|
|
1510
2237
|
return { error: 'Handler threw: ' + (e.message || String(e)), component: target.name };
|
|
1511
2238
|
}
|
|
1512
2239
|
})()
|
|
1513
2240
|
`;
|
|
1514
|
-
return executeInApp(expression, false);
|
|
2241
|
+
return executeInApp(expression, false, {}, options.device);
|
|
1515
2242
|
}
|
|
1516
2243
|
// ============================================================================
|
|
1517
2244
|
// Coordinate-Based Element Inspection (via DevTools Inspector API)
|
|
@@ -1520,7 +2247,7 @@ export async function pressElement(options) {
|
|
|
1520
2247
|
* Toggle the Element Inspector via DevSettings native module.
|
|
1521
2248
|
* This enables the inspector overlay programmatically.
|
|
1522
2249
|
*/
|
|
1523
|
-
export async function toggleElementInspector() {
|
|
2250
|
+
export async function toggleElementInspector(device) {
|
|
1524
2251
|
const expression = `
|
|
1525
2252
|
(function() {
|
|
1526
2253
|
const ds = globalThis.nativeModuleProxy?.DevSettings;
|
|
@@ -1539,12 +2266,12 @@ export async function toggleElementInspector() {
|
|
|
1539
2266
|
}
|
|
1540
2267
|
})()
|
|
1541
2268
|
`;
|
|
1542
|
-
return executeInApp(expression, false);
|
|
2269
|
+
return executeInApp(expression, false, {}, device);
|
|
1543
2270
|
}
|
|
1544
2271
|
/**
|
|
1545
2272
|
* Check if the Element Inspector overlay is currently active.
|
|
1546
2273
|
*/
|
|
1547
|
-
export async function isInspectorActive() {
|
|
2274
|
+
export async function isInspectorActive(device) {
|
|
1548
2275
|
const expression = `
|
|
1549
2276
|
(function() {
|
|
1550
2277
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -1560,7 +2287,7 @@ export async function isInspectorActive() {
|
|
|
1560
2287
|
if (roots.length === 0) return false;
|
|
1561
2288
|
|
|
1562
2289
|
function findComponent(fiber, targetName, depth = 0) {
|
|
1563
|
-
if (!fiber || depth >
|
|
2290
|
+
if (!fiber || depth > 5000) return null;
|
|
1564
2291
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1565
2292
|
if (name === targetName) return fiber;
|
|
1566
2293
|
let child = fiber.child;
|
|
@@ -1575,7 +2302,7 @@ export async function isInspectorActive() {
|
|
|
1575
2302
|
return !!findComponent(roots[0].current, 'InspectorPanel');
|
|
1576
2303
|
})()
|
|
1577
2304
|
`;
|
|
1578
|
-
const result = await executeInApp(expression, false);
|
|
2305
|
+
const result = await executeInApp(expression, false, {}, device);
|
|
1579
2306
|
if (result.success && result.result) {
|
|
1580
2307
|
return result.result === "true";
|
|
1581
2308
|
}
|
|
@@ -1586,7 +2313,7 @@ export async function isInspectorActive() {
|
|
|
1586
2313
|
* This reads the InspectorPanel component's props to get the hierarchy, frame, and style.
|
|
1587
2314
|
* Requires the Element Inspector to be enabled and an element to be selected.
|
|
1588
2315
|
*/
|
|
1589
|
-
export async function getInspectorSelection() {
|
|
2316
|
+
export async function getInspectorSelection(device) {
|
|
1590
2317
|
const expression = `
|
|
1591
2318
|
(function() {
|
|
1592
2319
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -1602,28 +2329,29 @@ export async function getInspectorSelection() {
|
|
|
1602
2329
|
}
|
|
1603
2330
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
1604
2331
|
|
|
1605
|
-
// Find InspectorPanel
|
|
1606
|
-
function
|
|
1607
|
-
if (!fiber || depth >
|
|
2332
|
+
// Find all InspectorPanel instances (apps with modals may have multiple)
|
|
2333
|
+
function findAllPanels(fiber, targetName, depth, results) {
|
|
2334
|
+
if (!fiber || depth > 5000) return;
|
|
1608
2335
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1609
|
-
if (name === targetName)
|
|
2336
|
+
if (name === targetName) results.push(fiber);
|
|
1610
2337
|
let child = fiber.child;
|
|
1611
2338
|
while (child) {
|
|
1612
|
-
|
|
1613
|
-
if (found) return found;
|
|
2339
|
+
findAllPanels(child, targetName, depth + 1, results);
|
|
1614
2340
|
child = child.sibling;
|
|
1615
2341
|
}
|
|
1616
|
-
return null;
|
|
1617
2342
|
}
|
|
1618
2343
|
|
|
1619
|
-
const
|
|
1620
|
-
|
|
2344
|
+
const panels = [];
|
|
2345
|
+
findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
|
|
2346
|
+
if (panels.length === 0) {
|
|
1621
2347
|
return {
|
|
1622
2348
|
error: 'Element Inspector is not active.',
|
|
1623
2349
|
hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
|
|
1624
2350
|
};
|
|
1625
2351
|
}
|
|
1626
2352
|
|
|
2353
|
+
// Prefer the panel that has an active selection
|
|
2354
|
+
const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
|
|
1627
2355
|
const props = panelFiber.memoizedProps;
|
|
1628
2356
|
if (!props.hierarchy || props.hierarchy.length === 0) {
|
|
1629
2357
|
return {
|
|
@@ -1659,7 +2387,7 @@ export async function getInspectorSelection() {
|
|
|
1659
2387
|
};
|
|
1660
2388
|
})()
|
|
1661
2389
|
`;
|
|
1662
|
-
return executeInApp(expression, false);
|
|
2390
|
+
return executeInApp(expression, false, {}, device);
|
|
1663
2391
|
}
|
|
1664
2392
|
/**
|
|
1665
2393
|
* Inspect the React component at a specific (x, y) coordinate.
|
|
@@ -1674,7 +2402,7 @@ export async function getInspectorSelection() {
|
|
|
1674
2402
|
* coordinates, return the innermost matching React component.
|
|
1675
2403
|
*/
|
|
1676
2404
|
export async function inspectAtPoint(x, y, options = {}) {
|
|
1677
|
-
const { includeProps = true, includeFrame = true } = options;
|
|
2405
|
+
const { includeProps = true, includeFrame = true, device } = options;
|
|
1678
2406
|
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
1679
2407
|
const dispatchExpression = `
|
|
1680
2408
|
(function() {
|
|
@@ -1736,7 +2464,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1736
2464
|
return { count: hostFibers.length };
|
|
1737
2465
|
})()
|
|
1738
2466
|
`;
|
|
1739
|
-
const dispatchResult = await executeInApp(dispatchExpression, false);
|
|
2467
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, {}, device);
|
|
1740
2468
|
if (!dispatchResult.success)
|
|
1741
2469
|
return dispatchResult;
|
|
1742
2470
|
try {
|
|
@@ -1782,7 +2510,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1782
2510
|
|
|
1783
2511
|
// RN primitives and internal components to skip when surfacing the "element" name.
|
|
1784
2512
|
// We want the nearest *custom* component, not a library wrapper.
|
|
1785
|
-
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|
|
|
2513
|
+
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|SafeAreaView|KeyboardAvoidingView|Animated\\(.*|withAnimated.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|VirtualizedList.*|CellRenderer.*|FrameSizeProvider|MaybeScreenContainer|RCT.*|RNS.*|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|Expo.*|LinearGradient|ViewManagerAdapter_.*|Svg.*|Defs|Path|Rect|Circle|G|Line|Polygon|Polyline|Ellipse|ClipPath|GestureHandler.*|NativeViewGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|MaybeScreen|SafeAreaProvider.*|GestureDetector|PanGestureHandler|DropShadow|BlurView|MaskedView.*)$/;
|
|
1786
2514
|
|
|
1787
2515
|
function getNearestNamed(fiber, skipPrimitives) {
|
|
1788
2516
|
var cur = fiber;
|
|
@@ -1872,6 +2600,6 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1872
2600
|
return result;
|
|
1873
2601
|
})()
|
|
1874
2602
|
`;
|
|
1875
|
-
return executeInApp(resolveExpression, false);
|
|
2603
|
+
return executeInApp(resolveExpression, false, {}, device);
|
|
1876
2604
|
}
|
|
1877
2605
|
//# sourceMappingURL=executor.js.map
|