react-native-ai-debugger 1.1.6 → 1.4.33
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 +386 -45
- 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/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/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 +542 -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 +104 -113
- 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 +115 -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 +82 -14
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +1869 -418
- 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/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 +66 -3
- package/build/core/guides.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 +22 -17
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +23 -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 +561 -326
- 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 +243 -0
- package/build/core/license.js.map +1 -0
- package/build/core/logbox.d.ts +57 -0
- package/build/core/logbox.d.ts.map +1 -0
- package/build/core/logbox.js +282 -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 +21 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +166 -10
- 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 +1490 -324
- 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 +84 -2
- package/build/pro/tap.d.ts.map +1 -1
- package/build/pro/tap.js +835 -129
- 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 +8 -7
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js +0 -2459
- 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 +0 -153
- 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 +0 -31
- package/build/httpServerStandalone.js.map +0 -1
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
|
|
@@ -148,16 +158,50 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
|
|
|
148
158
|
return { success: false, error: validation.error };
|
|
149
159
|
}
|
|
150
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) {
|
|
151
174
|
const TIMEOUT_MS = timeoutMs;
|
|
152
175
|
const currentMessageId = getNextMessageId();
|
|
153
|
-
// Prepend global polyfill for Hermes compatibility
|
|
154
|
-
// Runtime.evaluate returns the completion value of the last expression naturally,
|
|
155
|
-
// so no IIFE wrapping is needed (similar to browser console behavior)
|
|
156
176
|
const wrappedExpression = `${GLOBAL_POLYFILL} ${cleanedExpression}`;
|
|
157
177
|
return new Promise((resolve) => {
|
|
158
178
|
const timeoutId = setTimeout(() => {
|
|
159
179
|
pendingExecutions.delete(currentMessageId);
|
|
160
|
-
|
|
180
|
+
const wsState = app.ws.readyState === WebSocket.OPEN ? "OPEN"
|
|
181
|
+
: app.ws.readyState === WebSocket.CLOSED ? "CLOSED"
|
|
182
|
+
: app.ws.readyState === WebSocket.CLOSING ? "CLOSING"
|
|
183
|
+
: "CONNECTING";
|
|
184
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
185
|
+
const pageId = app.deviceInfo.id || "unknown";
|
|
186
|
+
const truncatedExpr = cleanedExpression.length > 100
|
|
187
|
+
? cleanedExpression.substring(0, 100) + "..."
|
|
188
|
+
: cleanedExpression;
|
|
189
|
+
const errorMessage = [
|
|
190
|
+
"Timeout: Expression took too long to evaluate.",
|
|
191
|
+
"",
|
|
192
|
+
`Connection state: ws=${wsState}, device="${deviceName}", platform=${app.platform}, pageId=${pageId}`,
|
|
193
|
+
`Expression (truncated): ${truncatedExpr}`,
|
|
194
|
+
"",
|
|
195
|
+
"This usually means the JavaScript execution context became unresponsive or the CDP page is stale.",
|
|
196
|
+
"",
|
|
197
|
+
"Recovery steps (try in order):",
|
|
198
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
199
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
200
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
201
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
202
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
203
|
+
].join("\n");
|
|
204
|
+
resolve({ success: false, error: errorMessage });
|
|
161
205
|
}, TIMEOUT_MS);
|
|
162
206
|
pendingExecutions.set(currentMessageId, { resolve, timeoutId });
|
|
163
207
|
try {
|
|
@@ -183,18 +227,75 @@ async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000
|
|
|
183
227
|
}
|
|
184
228
|
});
|
|
185
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
|
+
}
|
|
186
287
|
// Execute JavaScript in the connected React Native app with retry logic
|
|
187
|
-
export async function executeInApp(expression, awaitPromise = true, options = {}) {
|
|
288
|
+
export async function executeInApp(expression, awaitPromise = true, options = {}, device) {
|
|
188
289
|
const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
|
|
189
290
|
let lastError;
|
|
190
291
|
let preferredPort;
|
|
191
292
|
// Get preferred port from current connection if available
|
|
192
|
-
const currentApp =
|
|
293
|
+
const currentApp = getConnectedAppByDevice(device);
|
|
193
294
|
if (currentApp) {
|
|
194
295
|
preferredPort = currentApp.port;
|
|
195
296
|
}
|
|
196
297
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
197
|
-
const app =
|
|
298
|
+
const app = getConnectedAppByDevice(device);
|
|
198
299
|
// No connection - try to reconnect if enabled
|
|
199
300
|
if (!app) {
|
|
200
301
|
if (autoReconnect && attempt < maxRetries) {
|
|
@@ -262,11 +363,20 @@ export async function executeInApp(expression, awaitPromise = true, options = {}
|
|
|
262
363
|
}
|
|
263
364
|
return {
|
|
264
365
|
success: false,
|
|
265
|
-
error: lastError ??
|
|
366
|
+
error: lastError ?? [
|
|
367
|
+
"Execution failed after all retries. Connection may be stale.",
|
|
368
|
+
"",
|
|
369
|
+
"Recovery steps (try in order):",
|
|
370
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
371
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
372
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
373
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
374
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
375
|
+
].join("\n")
|
|
266
376
|
};
|
|
267
377
|
}
|
|
268
378
|
// List globally available debugging objects in the app
|
|
269
|
-
export async function listDebugGlobals() {
|
|
379
|
+
export async function listDebugGlobals(device) {
|
|
270
380
|
const expression = `
|
|
271
381
|
(function() {
|
|
272
382
|
const globals = Object.keys(globalThis);
|
|
@@ -282,10 +392,10 @@ export async function listDebugGlobals() {
|
|
|
282
392
|
return categories;
|
|
283
393
|
})()
|
|
284
394
|
`;
|
|
285
|
-
return executeInApp(expression, false);
|
|
395
|
+
return executeInApp(expression, false, {}, device);
|
|
286
396
|
}
|
|
287
397
|
// Inspect a global object to see its properties and types
|
|
288
|
-
export async function inspectGlobal(objectName) {
|
|
398
|
+
export async function inspectGlobal(objectName, device) {
|
|
289
399
|
const expression = `
|
|
290
400
|
(function() {
|
|
291
401
|
const obj = ${objectName};
|
|
@@ -305,14 +415,15 @@ export async function inspectGlobal(objectName) {
|
|
|
305
415
|
return result;
|
|
306
416
|
})()
|
|
307
417
|
`;
|
|
308
|
-
return executeInApp(expression, false);
|
|
418
|
+
return executeInApp(expression, false, {}, device);
|
|
309
419
|
}
|
|
310
|
-
// Reload the React Native app using __ReactRefresh
|
|
420
|
+
// Reload the React Native app using __ReactRefresh
|
|
421
|
+
// Note: Page.reload CDP method may work on Bridgeless targets (via HostAgent) — not yet tested
|
|
311
422
|
// Uses fire-and-forget: sends the reload command without waiting for a response,
|
|
312
423
|
// since the JS context is destroyed during reload and would always timeout.
|
|
313
|
-
export async function reloadApp() {
|
|
424
|
+
export async function reloadApp(device) {
|
|
314
425
|
// Get current connection info before reload
|
|
315
|
-
let app =
|
|
426
|
+
let app = getConnectedAppByDevice(device);
|
|
316
427
|
// Auto-connect if no connection exists
|
|
317
428
|
if (!app) {
|
|
318
429
|
console.error("[rn-ai-debugger] No connection for reload, attempting auto-connect...");
|
|
@@ -332,7 +443,7 @@ export async function reloadApp() {
|
|
|
332
443
|
try {
|
|
333
444
|
await connectToDevice(mainDevice, port);
|
|
334
445
|
console.error(`[rn-ai-debugger] Auto-connected to ${mainDevice.title} on port ${port}`);
|
|
335
|
-
app =
|
|
446
|
+
app = getConnectedAppByDevice(device);
|
|
336
447
|
break;
|
|
337
448
|
}
|
|
338
449
|
catch (error) {
|
|
@@ -366,7 +477,21 @@ export async function reloadApp() {
|
|
|
366
477
|
})()`;
|
|
367
478
|
try {
|
|
368
479
|
if (app.ws.readyState !== WebSocket.OPEN) {
|
|
369
|
-
|
|
480
|
+
const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: [
|
|
484
|
+
`WebSocket connection is not open (device="${deviceName}", platform=${app.platform}).`,
|
|
485
|
+
"The CDP page may be stale or the app has crashed.",
|
|
486
|
+
"",
|
|
487
|
+
"Recovery steps (try in order):",
|
|
488
|
+
"1. Call scan_metro to re-establish a fresh CDP connection",
|
|
489
|
+
"2. If scan_metro doesn't help, force-restart the app:",
|
|
490
|
+
" - iOS: ios_terminate_app then ios_launch_app",
|
|
491
|
+
" - Android: android_launch_app (restarts automatically)",
|
|
492
|
+
"3. After restarting, call scan_metro again to reconnect",
|
|
493
|
+
].join("\n")
|
|
494
|
+
};
|
|
370
495
|
}
|
|
371
496
|
// Send without registering a pending execution — fire and forget
|
|
372
497
|
const messageId = getNextMessageId();
|
|
@@ -391,11 +516,10 @@ export async function reloadApp() {
|
|
|
391
516
|
try {
|
|
392
517
|
// Wait for app to reload (give it time to restart JS context)
|
|
393
518
|
await delay(2000);
|
|
394
|
-
//
|
|
395
|
-
|
|
519
|
+
// Find and close only the targeted device's connection (not all devices on this port)
|
|
520
|
+
const targetDeviceId = app.deviceInfo.id;
|
|
396
521
|
for (const [key, connectedApp] of connectedApps.entries()) {
|
|
397
|
-
if (connectedApp.
|
|
398
|
-
// Cancel any pending reconnection timer BEFORE closing
|
|
522
|
+
if (connectedApp.deviceInfo.id === targetDeviceId) {
|
|
399
523
|
cancelReconnectionTimer(key);
|
|
400
524
|
try {
|
|
401
525
|
connectedApp.ws.close();
|
|
@@ -404,23 +528,23 @@ export async function reloadApp() {
|
|
|
404
528
|
// Ignore close errors
|
|
405
529
|
}
|
|
406
530
|
connectedApps.delete(key);
|
|
531
|
+
break;
|
|
407
532
|
}
|
|
408
533
|
}
|
|
409
534
|
// Small delay to ensure cleanup
|
|
410
535
|
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
|
|
536
|
+
// Reconnect only the reloaded device (not all devices on the port)
|
|
414
537
|
const devices = await fetchDevices(port);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
538
|
+
const targetDevice = devices.find(d => d.id === targetDeviceId)
|
|
539
|
+
|| devices.find(d => d.deviceName === app.deviceInfo.deviceName);
|
|
540
|
+
if (targetDevice) {
|
|
541
|
+
await connectToDevice(targetDevice, port, {
|
|
418
542
|
isReconnection: false,
|
|
419
543
|
reconnectionConfig: { ...DEFAULT_RECONNECTION_CONFIG, enabled: false }
|
|
420
544
|
});
|
|
421
545
|
return {
|
|
422
546
|
success: true,
|
|
423
|
-
result: `App reloaded and reconnected to ${
|
|
547
|
+
result: `App reloaded and reconnected to ${targetDevice.deviceName || targetDevice.title}`
|
|
424
548
|
};
|
|
425
549
|
}
|
|
426
550
|
else {
|
|
@@ -474,19 +598,184 @@ function formatTreeStructureOnly(node, indent = 0) {
|
|
|
474
598
|
}
|
|
475
599
|
return result;
|
|
476
600
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Classify each root as "screen" or "overlay".
|
|
603
|
+
*
|
|
604
|
+
* A root is a **screen** if its subtree contains a navigation screen marker
|
|
605
|
+
* (Route(...), *Screen, *Page). Falls back to the largest root by area
|
|
606
|
+
* if no navigation markers are found. Everything else is an overlay.
|
|
607
|
+
*/
|
|
608
|
+
function classifyRoots(roots, elements, childrenMap) {
|
|
609
|
+
const labels = [];
|
|
610
|
+
// Navigation screen markers: Route(...) wrapper or user-defined *Screen/*Page
|
|
611
|
+
const screenMarker = /^(Route\(|.*Screen\(|.*Page\(|.*Screen$|.*Page$)/;
|
|
612
|
+
function subtreeHasScreen(idx, depth) {
|
|
613
|
+
if (depth > 30)
|
|
614
|
+
return false;
|
|
615
|
+
if (screenMarker.test(elements[idx].component))
|
|
616
|
+
return true;
|
|
617
|
+
const kids = childrenMap.get(idx);
|
|
618
|
+
if (kids) {
|
|
619
|
+
for (const kid of kids) {
|
|
620
|
+
if (subtreeHasScreen(kid, depth + 1))
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
// First pass: check which roots contain a navigation screen
|
|
627
|
+
const hasScreen = roots.map(ri => subtreeHasScreen(ri, 0));
|
|
628
|
+
const anyScreenFound = hasScreen.some(Boolean);
|
|
629
|
+
if (anyScreenFound) {
|
|
630
|
+
for (let i = 0; i < roots.length; i++) {
|
|
631
|
+
labels.push(hasScreen[i] ? "screen" : "overlay");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// Fallback: largest root by area is the screen
|
|
636
|
+
let maxArea = 0;
|
|
637
|
+
let maxIdx = 0;
|
|
638
|
+
for (let i = 0; i < roots.length; i++) {
|
|
639
|
+
const f = elements[roots[i]].frame;
|
|
640
|
+
if (f && f.width * f.height > maxArea) {
|
|
641
|
+
maxArea = f.width * f.height;
|
|
642
|
+
maxIdx = i;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (let i = 0; i < roots.length; i++) {
|
|
646
|
+
labels.push(i === maxIdx ? "screen" : "overlay");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { labels, hasOverlays: labels.some(l => l === "overlay") };
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Build, collapse, classify, and render a layout tree.
|
|
653
|
+
*
|
|
654
|
+
* Shared by both get_screen_layout (points, no tap coords) and
|
|
655
|
+
* the screenshot layout enrichment (pixels, with tap coords).
|
|
656
|
+
*
|
|
657
|
+
* @param elements - flat list of layout nodes with parentIndex linkage
|
|
658
|
+
* @param renderLine - callback that produces the output line for a node,
|
|
659
|
+
* given (element, indent level, whether it's a leaf)
|
|
660
|
+
*/
|
|
661
|
+
function formatLayoutTree(elements, renderLine) {
|
|
662
|
+
// Build index: originalIndex -> element index in the filtered array
|
|
663
|
+
const indexMap = new Map();
|
|
664
|
+
for (let i = 0; i < elements.length; i++) {
|
|
665
|
+
if (elements[i].originalIndex !== undefined) {
|
|
666
|
+
indexMap.set(elements[i].originalIndex, i);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Build children lists
|
|
670
|
+
const children = new Map();
|
|
671
|
+
const roots = [];
|
|
672
|
+
for (let i = 0; i < elements.length; i++) {
|
|
673
|
+
const el = elements[i];
|
|
674
|
+
const parentOrigIdx = el.parentIndex;
|
|
675
|
+
if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
|
|
676
|
+
roots.push(i);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
const parentIdx = indexMap.get(parentOrigIdx);
|
|
680
|
+
if (!children.has(parentIdx))
|
|
681
|
+
children.set(parentIdx, []);
|
|
682
|
+
children.get(parentIdx).push(i);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Collapse wrapper chains: if a child has the same frame as its parent,
|
|
686
|
+
// it's just a wrapper — promote its children and text to the parent
|
|
687
|
+
function sameFrame(a, b) {
|
|
688
|
+
if (!a.frame || !b.frame)
|
|
689
|
+
return false;
|
|
690
|
+
return Math.abs(a.frame.x - b.frame.x) < 1 &&
|
|
691
|
+
Math.abs(a.frame.y - b.frame.y) < 1 &&
|
|
692
|
+
Math.abs(a.frame.width - b.frame.width) < 1 &&
|
|
693
|
+
Math.abs(a.frame.height - b.frame.height) < 1;
|
|
694
|
+
}
|
|
695
|
+
function collapseNode(parentIdx) {
|
|
696
|
+
const kids = children.get(parentIdx);
|
|
697
|
+
if (!kids)
|
|
698
|
+
return;
|
|
699
|
+
const newKids = [];
|
|
700
|
+
for (const kidIdx of kids) {
|
|
701
|
+
if (sameFrame(elements[parentIdx], elements[kidIdx])) {
|
|
702
|
+
if (!elements[parentIdx].text && elements[kidIdx].text) {
|
|
703
|
+
elements[parentIdx].text = elements[kidIdx].text;
|
|
704
|
+
}
|
|
705
|
+
const grandKids = children.get(kidIdx);
|
|
706
|
+
if (grandKids) {
|
|
707
|
+
newKids.push(...grandKids);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
newKids.push(kidIdx);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (newKids.length > 0) {
|
|
715
|
+
children.set(parentIdx, newKids);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
children.delete(parentIdx);
|
|
719
|
+
}
|
|
720
|
+
const updatedKids = children.get(parentIdx);
|
|
721
|
+
if (updatedKids) {
|
|
722
|
+
for (const kid of updatedKids) {
|
|
723
|
+
collapseNode(kid);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
for (const root of roots) {
|
|
728
|
+
collapseNode(root);
|
|
729
|
+
}
|
|
730
|
+
// Render tree
|
|
731
|
+
const lines = [];
|
|
732
|
+
function printNode(idx, indent) {
|
|
733
|
+
const isLeaf = !children.has(idx);
|
|
734
|
+
lines.push(renderLine(elements[idx], indent, isLeaf));
|
|
735
|
+
const kids = children.get(idx);
|
|
736
|
+
if (kids) {
|
|
737
|
+
for (const kid of kids) {
|
|
738
|
+
printNode(kid, indent + 1);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// Classify roots and emit layer headers
|
|
743
|
+
const { labels, hasOverlays } = classifyRoots(roots, elements, children);
|
|
744
|
+
if (hasOverlays) {
|
|
745
|
+
let prevLabel = "";
|
|
746
|
+
for (let ri = 0; ri < roots.length; ri++) {
|
|
747
|
+
const label = labels[ri];
|
|
748
|
+
if (label === "overlay" || label !== prevLabel) {
|
|
749
|
+
if (lines.length > 0)
|
|
750
|
+
lines.push("");
|
|
751
|
+
lines.push(`[${label}]`);
|
|
752
|
+
}
|
|
753
|
+
prevLabel = label;
|
|
754
|
+
printNode(roots[ri], 0);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
for (const root of roots) {
|
|
759
|
+
printNode(root, 0);
|
|
760
|
+
}
|
|
487
761
|
}
|
|
488
762
|
return lines.join("\n");
|
|
489
763
|
}
|
|
764
|
+
function formatScreenLayoutTree(elements, extended = false) {
|
|
765
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
766
|
+
const prefix = " ".repeat(indent);
|
|
767
|
+
const frame = el.frame
|
|
768
|
+
? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
|
|
769
|
+
: "";
|
|
770
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
771
|
+
const idStr = id ? ` [${id}]` : "";
|
|
772
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
773
|
+
const layoutStr = extended && el.layout
|
|
774
|
+
? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
|
|
775
|
+
: "";
|
|
776
|
+
return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
490
779
|
function formatFoundComponentsToTonl(components) {
|
|
491
780
|
const lines = ["#found{component,path,depth,key,layout}"];
|
|
492
781
|
for (const c of components) {
|
|
@@ -511,11 +800,11 @@ function formatSummaryToTonl(components, total) {
|
|
|
511
800
|
* This traverses the fiber tree to extract component hierarchy with names.
|
|
512
801
|
*/
|
|
513
802
|
export async function getComponentTree(options = {}) {
|
|
514
|
-
const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false } = options;
|
|
803
|
+
const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false, device } = options;
|
|
515
804
|
// Use lower default depth for structureOnly to keep output compact (~2-5KB)
|
|
516
805
|
// Full mode uses higher depth since TONL format handles it better
|
|
517
806
|
// focusedOnly mode uses moderate depth since we're already filtering to active screen
|
|
518
|
-
const maxDepth = options.maxDepth ??
|
|
807
|
+
const maxDepth = options.maxDepth ?? 5000;
|
|
519
808
|
const expression = `
|
|
520
809
|
(function() {
|
|
521
810
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -576,29 +865,34 @@ export async function getComponentTree(options = {}) {
|
|
|
576
865
|
}
|
|
577
866
|
|
|
578
867
|
function extractLayoutStyles(style) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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; }
|
|
602
896
|
}
|
|
603
897
|
|
|
604
898
|
function walkFiber(fiber, depth) {
|
|
@@ -627,23 +921,29 @@ export async function getComponentTree(options = {}) {
|
|
|
627
921
|
const props = {};
|
|
628
922
|
for (const key of Object.keys(fiber.memoizedProps)) {
|
|
629
923
|
if (key === 'children' || key === 'style') continue;
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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]';
|
|
637
935
|
}
|
|
638
936
|
}
|
|
639
937
|
if (Object.keys(props).length > 0) node.props = props;
|
|
640
938
|
}
|
|
641
939
|
|
|
642
940
|
// Include layout styles if requested
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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 */ }
|
|
647
947
|
|
|
648
948
|
// Traverse children
|
|
649
949
|
let child = fiber.child;
|
|
@@ -660,7 +960,7 @@ export async function getComponentTree(options = {}) {
|
|
|
660
960
|
|
|
661
961
|
// Find focused screen if requested
|
|
662
962
|
function findFocusedScreen(fiber, depth = 0) {
|
|
663
|
-
if (!fiber || depth >
|
|
963
|
+
if (!fiber || depth > 5000) return null;
|
|
664
964
|
|
|
665
965
|
const name = getComponentName(fiber);
|
|
666
966
|
|
|
@@ -710,7 +1010,7 @@ export async function getComponentTree(options = {}) {
|
|
|
710
1010
|
})()
|
|
711
1011
|
`;
|
|
712
1012
|
// Use a longer timeout for component tree traversal — large apps can exceed 10s
|
|
713
|
-
const result = await executeInApp(expression, false, { timeoutMs: 30000 });
|
|
1013
|
+
const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
|
|
714
1014
|
// Apply formatting if requested
|
|
715
1015
|
if (result.success && result.result) {
|
|
716
1016
|
try {
|
|
@@ -736,33 +1036,57 @@ export async function getComponentTree(options = {}) {
|
|
|
736
1036
|
return result;
|
|
737
1037
|
}
|
|
738
1038
|
/**
|
|
739
|
-
* Get layout
|
|
740
|
-
*
|
|
1039
|
+
* Get layout data for visible components on the current screen.
|
|
1040
|
+
* Uses measureInWindow to get actual screen positions and filters
|
|
1041
|
+
* to only components within the viewport.
|
|
1042
|
+
*
|
|
1043
|
+
* Two-step approach (same as inspectAtPoint):
|
|
1044
|
+
* Step 1: Walk fiber tree, dispatch measureInWindow on host components
|
|
1045
|
+
* Step 2: After 300ms, read measurements, filter by viewport, build results
|
|
741
1046
|
*/
|
|
742
1047
|
export async function getScreenLayout(options = {}) {
|
|
743
|
-
const {
|
|
744
|
-
const
|
|
1048
|
+
const { extended = false, summary = false, device, raw = false } = options;
|
|
1049
|
+
const maxDepth = 5000;
|
|
1050
|
+
const componentsOnly = true;
|
|
1051
|
+
const shortPath = true;
|
|
1052
|
+
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
1053
|
+
const dispatchExpression = `
|
|
745
1054
|
(function() {
|
|
746
|
-
|
|
1055
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
747
1056
|
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
748
1057
|
|
|
749
|
-
|
|
1058
|
+
var roots = [];
|
|
750
1059
|
if (hook.getFiberRoots) {
|
|
751
|
-
roots =
|
|
1060
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
752
1061
|
}
|
|
753
1062
|
if (roots.length === 0 && hook.renderers) {
|
|
754
|
-
for (
|
|
755
|
-
|
|
1063
|
+
for (var entry of hook.renderers) {
|
|
1064
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
756
1065
|
if (r.length > 0) { roots = r; break; }
|
|
757
1066
|
}
|
|
758
1067
|
}
|
|
759
1068
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
760
1069
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1070
|
+
function getMeasurable(fiber) {
|
|
1071
|
+
var sn = fiber.stateNode;
|
|
1072
|
+
if (!sn) return null;
|
|
1073
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
1074
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
1075
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
1076
|
+
return sn.canonical.publicInstance;
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Collect host fibers with their metadata
|
|
1082
|
+
var hostFibers = [];
|
|
1083
|
+
var fiberMeta = [];
|
|
1084
|
+
var componentsOnlyMode = ${componentsOnly};
|
|
1085
|
+
|
|
1086
|
+
// RN internals and primitives — skip these when looking for meaningful component names
|
|
1087
|
+
// Only filter internal components that can't be written as JSX.
|
|
1088
|
+
// Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
|
|
1089
|
+
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
1090
|
|
|
767
1091
|
function getComponentName(fiber) {
|
|
768
1092
|
if (!fiber || !fiber.type) return null;
|
|
@@ -770,116 +1094,349 @@ export async function getScreenLayout(options = {}) {
|
|
|
770
1094
|
return fiber.type.displayName || fiber.type.name || null;
|
|
771
1095
|
}
|
|
772
1096
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1097
|
+
// Find the first measurable host descendant of a fiber
|
|
1098
|
+
function findFirstHost(fiber, depth) {
|
|
1099
|
+
if (!fiber || depth > 20) return null;
|
|
1100
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
|
|
1101
|
+
var child = fiber.child;
|
|
1102
|
+
while (child) {
|
|
1103
|
+
var found = findFirstHost(child, depth + 1);
|
|
1104
|
+
if (found) return found;
|
|
1105
|
+
child = child.sibling;
|
|
780
1106
|
}
|
|
781
|
-
return
|
|
1107
|
+
return null;
|
|
782
1108
|
}
|
|
783
1109
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1110
|
+
// Extract text content from a fiber subtree.
|
|
1111
|
+
// When a fiber has a string child, return it without recursing
|
|
1112
|
+
// (avoids duplication from Text > RCTText having the same string).
|
|
1113
|
+
function collectText(fiber, d) {
|
|
1114
|
+
if (!fiber || d > 30) return '';
|
|
1115
|
+
var props = fiber.memoizedProps;
|
|
1116
|
+
if (props) {
|
|
1117
|
+
var ch = props.children;
|
|
1118
|
+
// Leaf text — return without recursing into children fibers
|
|
1119
|
+
if (typeof ch === 'string') return ch;
|
|
1120
|
+
if (typeof ch === 'number') return String(ch);
|
|
1121
|
+
if (Array.isArray(ch)) {
|
|
1122
|
+
var inline = [];
|
|
1123
|
+
for (var ci = 0; ci < ch.length; ci++) {
|
|
1124
|
+
if (typeof ch[ci] === 'string') inline.push(ch[ci]);
|
|
1125
|
+
else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
|
|
1126
|
+
}
|
|
1127
|
+
if (inline.length > 0) return inline.join('');
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// No direct text — collect from child fibers (siblings = adjacent elements)
|
|
1131
|
+
var parts = [];
|
|
1132
|
+
var child = fiber.child;
|
|
1133
|
+
while (child) {
|
|
1134
|
+
var t = collectText(child, d + 1);
|
|
1135
|
+
if (t) parts.push(t);
|
|
1136
|
+
child = child.sibling;
|
|
1137
|
+
}
|
|
1138
|
+
return parts.join(' ').trim();
|
|
790
1139
|
}
|
|
791
1140
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1141
|
+
if (componentsOnlyMode) {
|
|
1142
|
+
// componentsOnly: walk tree looking for meaningful custom components,
|
|
1143
|
+
// measure their first host child, track parent for tree output
|
|
1144
|
+
function walkComponents(fiber, depth, path, parentIdx) {
|
|
1145
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1146
|
+
var name = getComponentName(fiber);
|
|
1147
|
+
var isHost = typeof fiber.type === 'string';
|
|
1148
|
+
|
|
1149
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1150
|
+
// active: 0 = inactive/detached, 1 = transitioning, 2 = active
|
|
1151
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1152
|
+
|
|
1153
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1154
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1155
|
+
|
|
1156
|
+
var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
|
|
1157
|
+
|
|
1158
|
+
var myIdx = parentIdx;
|
|
1159
|
+
if (isMeaningful) {
|
|
1160
|
+
var host = findFirstHost(fiber, 0);
|
|
1161
|
+
if (host) {
|
|
1162
|
+
myIdx = hostFibers.length;
|
|
1163
|
+
// Extract text from this component's subtree
|
|
1164
|
+
var text = collectText(fiber, 0);
|
|
1165
|
+
hostFibers.push(host);
|
|
1166
|
+
fiberMeta.push({
|
|
1167
|
+
hostName: typeof host.type === 'string' ? host.type : '',
|
|
1168
|
+
customName: name,
|
|
1169
|
+
depth: depth,
|
|
1170
|
+
path: path.concat([name]),
|
|
1171
|
+
parentIndex: parentIdx,
|
|
1172
|
+
text: text ? text.slice(0, 80) : null
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
820
1176
|
|
|
821
|
-
|
|
822
|
-
|
|
1177
|
+
var child = fiber.child;
|
|
1178
|
+
while (child) {
|
|
1179
|
+
var childName = getComponentName(child);
|
|
1180
|
+
walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
|
|
1181
|
+
child = child.sibling;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
walkComponents(roots[0].current, 0, [], -1);
|
|
1185
|
+
} else {
|
|
1186
|
+
// Default mode: collect all host fibers with ancestor info
|
|
1187
|
+
function walkFibers(fiber, depth, path) {
|
|
1188
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1189
|
+
var name = getComponentName(fiber);
|
|
1190
|
+
var isHost = typeof fiber.type === 'string';
|
|
1191
|
+
|
|
1192
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1193
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1194
|
+
|
|
1195
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1196
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1197
|
+
|
|
1198
|
+
if (name && isHost && getMeasurable(fiber)) {
|
|
1199
|
+
// Find nearest meaningful custom component ancestor for display
|
|
1200
|
+
var customName = null;
|
|
1201
|
+
var fallbackName = null;
|
|
1202
|
+
var cur = fiber.return;
|
|
1203
|
+
while (cur) {
|
|
1204
|
+
if (cur.type && typeof cur.type !== 'string') {
|
|
1205
|
+
var cName = cur.type.displayName || cur.type.name || null;
|
|
1206
|
+
if (cName) {
|
|
1207
|
+
if (!fallbackName) fallbackName = cName;
|
|
1208
|
+
if (!RN_PRIMITIVES.test(cName)) {
|
|
1209
|
+
customName = cName;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
cur = cur.return;
|
|
1215
|
+
}
|
|
1216
|
+
if (!customName) customName = fallbackName;
|
|
1217
|
+
|
|
1218
|
+
hostFibers.push(fiber);
|
|
1219
|
+
fiberMeta.push({
|
|
1220
|
+
hostName: name,
|
|
1221
|
+
customName: customName,
|
|
1222
|
+
depth: depth,
|
|
1223
|
+
path: path.slice()
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
823
1226
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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);
|
|
1227
|
+
var child = fiber.child;
|
|
1228
|
+
while (child) {
|
|
1229
|
+
var childName = getComponentName(child);
|
|
1230
|
+
walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1231
|
+
child = child.sibling;
|
|
838
1232
|
}
|
|
1233
|
+
}
|
|
1234
|
+
walkFibers(roots[0].current, 0, []);
|
|
1235
|
+
}
|
|
839
1236
|
|
|
840
|
-
|
|
841
|
-
component: name,
|
|
842
|
-
path: formatPath(path),
|
|
843
|
-
depth
|
|
844
|
-
};
|
|
1237
|
+
if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
|
|
845
1238
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1239
|
+
// Store fibers and metadata globally for step 2
|
|
1240
|
+
globalThis.__layoutFibers = hostFibers;
|
|
1241
|
+
globalThis.__layoutMeta = fiberMeta;
|
|
1242
|
+
globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
|
|
1243
|
+
|
|
1244
|
+
// Dispatch measureInWindow on all host fibers
|
|
1245
|
+
for (var i = 0; i < hostFibers.length; i++) {
|
|
1246
|
+
try {
|
|
1247
|
+
(function(idx) {
|
|
1248
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
1249
|
+
globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
1250
|
+
});
|
|
1251
|
+
})(i);
|
|
1252
|
+
} catch(e) {}
|
|
1253
|
+
}
|
|
858
1254
|
|
|
859
|
-
|
|
1255
|
+
return { count: hostFibers.length };
|
|
1256
|
+
})()
|
|
1257
|
+
`;
|
|
1258
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
|
|
1259
|
+
if (!dispatchResult.success)
|
|
1260
|
+
return dispatchResult;
|
|
1261
|
+
try {
|
|
1262
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
1263
|
+
if (parsed.error)
|
|
1264
|
+
return { success: false, error: parsed.error };
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
/* ignore */
|
|
1268
|
+
}
|
|
1269
|
+
// Wait for measureInWindow callbacks
|
|
1270
|
+
await delay(300);
|
|
1271
|
+
// --- Step 2: read measurements, filter visible, build results ---
|
|
1272
|
+
const resolveExpression = `
|
|
1273
|
+
(function() {
|
|
1274
|
+
var fibers = globalThis.__layoutFibers;
|
|
1275
|
+
var meta = globalThis.__layoutMeta;
|
|
1276
|
+
var measurements = globalThis.__layoutMeasurements;
|
|
1277
|
+
globalThis.__layoutFibers = null;
|
|
1278
|
+
globalThis.__layoutMeta = null;
|
|
1279
|
+
globalThis.__layoutMeasurements = null;
|
|
1280
|
+
|
|
1281
|
+
if (!fibers || !measurements || !meta) {
|
|
1282
|
+
return { error: 'No measurement data. Run get_screen_layout again.' };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
var componentsOnly = ${componentsOnly};
|
|
1286
|
+
var shortPath = ${shortPath};
|
|
1287
|
+
var summaryMode = ${summary};
|
|
1288
|
+
var pathSegments = 3;
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
// Get viewport dimensions from the first root view measurement
|
|
1292
|
+
// Accept elements starting at x=0 even with negative y (safe area extensions)
|
|
1293
|
+
var viewportW = 9999, viewportH = 9999;
|
|
1294
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
1295
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
|
|
1296
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
1297
|
+
viewportW = measurements[v].width;
|
|
1298
|
+
// For wrappers extending behind safe area, the visible viewport height
|
|
1299
|
+
// is the total height minus the negative offset
|
|
1300
|
+
viewportH = measurements[v].height + measurements[v].y;
|
|
1301
|
+
break;
|
|
860
1302
|
}
|
|
1303
|
+
}
|
|
861
1304
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const childName = getComponentName(child);
|
|
866
|
-
walkFiber(child, depth + 1, childName ? [...path, childName] : path);
|
|
867
|
-
child = child.sibling;
|
|
1305
|
+
function formatPath(pathArray) {
|
|
1306
|
+
if (!shortPath || pathArray.length <= pathSegments) {
|
|
1307
|
+
return pathArray.join(' > ');
|
|
868
1308
|
}
|
|
1309
|
+
return '... > ' + pathArray.slice(-pathSegments).join(' > ');
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function extractLayoutStyles(style) {
|
|
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; }
|
|
869
1343
|
}
|
|
870
1344
|
|
|
871
|
-
|
|
1345
|
+
var elements = [];
|
|
1346
|
+
|
|
1347
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
1348
|
+
var m = measurements[i];
|
|
1349
|
+
if (!m) continue;
|
|
1350
|
+
|
|
1351
|
+
// Filter: only visible within viewport (with some tolerance for partial visibility)
|
|
1352
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
1353
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
1354
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
1355
|
+
|
|
1356
|
+
var fiber = fibers[i];
|
|
1357
|
+
var info = meta[i];
|
|
1358
|
+
|
|
1359
|
+
var displayName = componentsOnly ? info.customName : info.hostName;
|
|
1360
|
+
if (!displayName) continue;
|
|
1361
|
+
if (componentsOnly && !info.customName) continue;
|
|
1362
|
+
|
|
1363
|
+
var style = null;
|
|
1364
|
+
try { style = fiber.memoizedProps ? fiber.memoizedProps.style : null; } catch {}
|
|
1365
|
+
var layout = extractLayoutStyles(style);
|
|
1366
|
+
|
|
1367
|
+
// Get text content — use pre-collected text from step 1, or fall back to host fiber
|
|
1368
|
+
var textContent = info.text || null;
|
|
1369
|
+
if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
|
|
1370
|
+
var children = null;
|
|
1371
|
+
try { children = fiber.memoizedProps ? fiber.memoizedProps.children : null; } catch {}
|
|
1372
|
+
if (typeof children === 'string') textContent = children;
|
|
1373
|
+
else if (typeof children === 'number') textContent = String(children);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
var element = {
|
|
1377
|
+
component: displayName,
|
|
1378
|
+
path: formatPath(info.path),
|
|
1379
|
+
depth: info.depth,
|
|
1380
|
+
frame: { x: m.x, y: m.y, width: m.width, height: m.height },
|
|
1381
|
+
originalIndex: i
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
|
|
1385
|
+
if (layout) element.layout = layout;
|
|
1386
|
+
if (textContent) element.text = textContent.slice(0, 100);
|
|
1387
|
+
|
|
1388
|
+
// Identifiers
|
|
1389
|
+
if (fiber.memoizedProps) {
|
|
1390
|
+
var identifiers = {};
|
|
1391
|
+
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
1392
|
+
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
1393
|
+
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
1394
|
+
if (fiber.key) identifiers.key = fiber.key;
|
|
1395
|
+
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
elements.push(element);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// In componentsOnly mode, remove full-screen wrapper components
|
|
1402
|
+
// and re-parent their children to collapse the wrapper chain
|
|
1403
|
+
if (componentsOnly && elements.length > 0) {
|
|
1404
|
+
var filtered = [];
|
|
1405
|
+
// Map: originalIndex -> resolved parent for skipped wrappers
|
|
1406
|
+
var reparent = {};
|
|
1407
|
+
|
|
1408
|
+
for (var fi = 0; fi < elements.length; fi++) {
|
|
1409
|
+
var el = elements[fi];
|
|
1410
|
+
var fr = el.frame;
|
|
1411
|
+
// A wrapper is full-screen if it covers the entire viewport or extends beyond it
|
|
1412
|
+
// (e.g., y=-119 wrappers that extend behind the safe area)
|
|
1413
|
+
var isFullScreen = fr && fr.x <= 0 && fr.y <= 0 &&
|
|
1414
|
+
(fr.width >= viewportW - 2) && (fr.y + fr.height >= viewportH - 2);
|
|
1415
|
+
|
|
1416
|
+
if (isFullScreen) {
|
|
1417
|
+
// Skip this wrapper, map its originalIndex to its parent
|
|
1418
|
+
reparent[el.originalIndex] = el.parentIndex;
|
|
1419
|
+
} else {
|
|
1420
|
+
// Resolve parent through any skipped wrappers
|
|
1421
|
+
var resolvedParent = el.parentIndex;
|
|
1422
|
+
while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
|
|
1423
|
+
resolvedParent = reparent[resolvedParent];
|
|
1424
|
+
}
|
|
1425
|
+
el.parentIndex = resolvedParent;
|
|
1426
|
+
filtered.push(el);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
elements = filtered;
|
|
1430
|
+
}
|
|
872
1431
|
|
|
873
|
-
// Summary mode: return counts by component name
|
|
874
1432
|
if (summaryMode) {
|
|
875
|
-
|
|
876
|
-
for (
|
|
877
|
-
counts[
|
|
1433
|
+
var counts = {};
|
|
1434
|
+
for (var j = 0; j < elements.length; j++) {
|
|
1435
|
+
counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
|
|
878
1436
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
.map(([name, count]) => ({ component: name, count }));
|
|
1437
|
+
var sorted = Object.keys(counts).map(function(name) {
|
|
1438
|
+
return { component: name, count: counts[name] };
|
|
1439
|
+
}).sort(function(a, b) { return b.count - a.count; });
|
|
883
1440
|
return {
|
|
884
1441
|
totalElements: elements.length,
|
|
885
1442
|
uniqueComponents: sorted.length,
|
|
@@ -888,15 +1445,15 @@ export async function getScreenLayout(options = {}) {
|
|
|
888
1445
|
}
|
|
889
1446
|
|
|
890
1447
|
return {
|
|
1448
|
+
viewport: { width: viewportW, height: viewportH },
|
|
891
1449
|
totalElements: elements.length,
|
|
892
1450
|
elements: elements
|
|
893
1451
|
};
|
|
894
1452
|
})()
|
|
895
1453
|
`;
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (format === "tonl" && result.success && result.result) {
|
|
1454
|
+
const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
|
|
1455
|
+
// Format output as tree
|
|
1456
|
+
if (result.success && result.result) {
|
|
900
1457
|
try {
|
|
901
1458
|
const parsed = JSON.parse(result.result);
|
|
902
1459
|
if (parsed.components) {
|
|
@@ -905,9 +1462,16 @@ export async function getScreenLayout(options = {}) {
|
|
|
905
1462
|
return { success: true, result: tonl };
|
|
906
1463
|
}
|
|
907
1464
|
else if (parsed.elements) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1465
|
+
if (raw) {
|
|
1466
|
+
return {
|
|
1467
|
+
success: true,
|
|
1468
|
+
result: result.result,
|
|
1469
|
+
parsedElements: parsed.elements,
|
|
1470
|
+
viewport: parsed.viewport
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
const tree = formatScreenLayoutTree(parsed.elements, extended);
|
|
1474
|
+
return { success: true, result: tree };
|
|
911
1475
|
}
|
|
912
1476
|
}
|
|
913
1477
|
catch {
|
|
@@ -916,39 +1480,534 @@ export async function getScreenLayout(options = {}) {
|
|
|
916
1480
|
}
|
|
917
1481
|
return result;
|
|
918
1482
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true } = options;
|
|
924
|
-
const escapedName = componentName.replace(/'/g, "\\'");
|
|
925
|
-
const expression = `
|
|
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 = `
|
|
926
1487
|
(function() {
|
|
927
|
-
|
|
1488
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
928
1489
|
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
929
1490
|
|
|
930
|
-
|
|
1491
|
+
var roots = [];
|
|
931
1492
|
if (hook.getFiberRoots) {
|
|
932
|
-
roots =
|
|
1493
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
933
1494
|
}
|
|
934
1495
|
if (roots.length === 0 && hook.renderers) {
|
|
935
|
-
for (
|
|
936
|
-
|
|
1496
|
+
for (var entry of hook.renderers) {
|
|
1497
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
937
1498
|
if (r.length > 0) { roots = r; break; }
|
|
938
1499
|
}
|
|
939
1500
|
}
|
|
940
1501
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
941
1502
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
+
}
|
|
950
1513
|
|
|
951
|
-
function
|
|
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
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Format enriched elements as an indented tree with tap coordinates in pixels.
|
|
1915
|
+
* Same tree structure as get_screen_layout but with tap:(x,y) per node.
|
|
1916
|
+
*/
|
|
1917
|
+
function formatEnrichedLayoutTree(elements) {
|
|
1918
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
1919
|
+
const prefix = " ".repeat(indent);
|
|
1920
|
+
const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
|
|
1921
|
+
const tap = ` tap:(${el.tapX},${el.tapY})`;
|
|
1922
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
1923
|
+
const idStr = id ? ` [${id}]` : "";
|
|
1924
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
1925
|
+
return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
|
|
1930
|
+
* Converts points/dp frame coordinates to pixels using the device pixel ratio,
|
|
1931
|
+
* computes center-point tapX/tapY for each element.
|
|
1932
|
+
*
|
|
1933
|
+
* @param pixelRatio - device pixel ratio (e.g., 3 for @3x iPhone)
|
|
1934
|
+
* @param screenshotScaleFactor - if the screenshot image was scaled down, this factor adjusts coordinates
|
|
1935
|
+
* @param device - optional target device name
|
|
1936
|
+
* @returns formatted tree string with pixel coordinates, or null if unavailable
|
|
1937
|
+
*/
|
|
1938
|
+
export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device) {
|
|
1939
|
+
try {
|
|
1940
|
+
const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
|
|
1941
|
+
if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
|
|
1942
|
+
return null;
|
|
1943
|
+
const elements = result.parsedElements.map((el) => {
|
|
1944
|
+
const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
|
|
1945
|
+
// Convert center point from points/dp to screenshot pixels
|
|
1946
|
+
// Points -> device pixels: multiply by pixelRatio
|
|
1947
|
+
// Device pixels -> screenshot pixels: divide by screenshotScaleFactor (if image was resized)
|
|
1948
|
+
const centerXPoints = frame.x + frame.width / 2;
|
|
1949
|
+
const centerYPoints = frame.y + frame.height / 2;
|
|
1950
|
+
const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
|
|
1951
|
+
const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
|
|
1952
|
+
// Convert frame to pixels too
|
|
1953
|
+
const pixelFrame = {
|
|
1954
|
+
x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
|
|
1955
|
+
y: Math.round((frame.y * pixelRatio) / screenshotScaleFactor),
|
|
1956
|
+
width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
|
|
1957
|
+
height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
|
|
1958
|
+
};
|
|
1959
|
+
return {
|
|
1960
|
+
component: el.component,
|
|
1961
|
+
frame: pixelFrame,
|
|
1962
|
+
tapX,
|
|
1963
|
+
tapY,
|
|
1964
|
+
text: el.text,
|
|
1965
|
+
identifiers: el.identifiers,
|
|
1966
|
+
parentIndex: el.parentIndex,
|
|
1967
|
+
originalIndex: el.originalIndex,
|
|
1968
|
+
depth: el.depth,
|
|
1969
|
+
path: el.path,
|
|
1970
|
+
};
|
|
1971
|
+
});
|
|
1972
|
+
return formatEnrichedLayoutTree(elements);
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
return null; // Non-fatal: screenshot works without layout
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Inspect a specific component by name, returning its props, state, and layout.
|
|
1980
|
+
*/
|
|
1981
|
+
export async function inspectComponent(componentName, options = {}) {
|
|
1982
|
+
const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true, device } = options;
|
|
1983
|
+
const escapedName = componentName.replace(/'/g, "\\'");
|
|
1984
|
+
const expression = `
|
|
1985
|
+
(function() {
|
|
1986
|
+
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1987
|
+
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
1988
|
+
|
|
1989
|
+
let roots = [];
|
|
1990
|
+
if (hook.getFiberRoots) {
|
|
1991
|
+
roots = [...(hook.getFiberRoots(1) || [])];
|
|
1992
|
+
}
|
|
1993
|
+
if (roots.length === 0 && hook.renderers) {
|
|
1994
|
+
for (const [id] of hook.renderers) {
|
|
1995
|
+
const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
|
|
1996
|
+
if (r.length > 0) { roots = r; break; }
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
2000
|
+
|
|
2001
|
+
const targetName = '${escapedName}';
|
|
2002
|
+
const targetIndex = ${index};
|
|
2003
|
+
const includeState = ${includeState};
|
|
2004
|
+
const includeChildren = ${includeChildren};
|
|
2005
|
+
const childrenDepth = ${childrenDepth};
|
|
2006
|
+
const shortPath = ${shortPath};
|
|
2007
|
+
const simplifyHooks = ${simplifyHooks};
|
|
2008
|
+
const pathSegments = 3;
|
|
2009
|
+
|
|
2010
|
+
function getComponentName(fiber) {
|
|
952
2011
|
if (!fiber || !fiber.type) return null;
|
|
953
2012
|
if (typeof fiber.type === 'string') return fiber.type;
|
|
954
2013
|
return fiber.type.displayName || fiber.type.name || null;
|
|
@@ -962,11 +2021,16 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
962
2021
|
}
|
|
963
2022
|
|
|
964
2023
|
function extractStyles(style) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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]' }; }
|
|
970
2034
|
}
|
|
971
2035
|
|
|
972
2036
|
function serializeValue(val, depth = 0) {
|
|
@@ -984,7 +2048,11 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
984
2048
|
if (keys.length > 20) return '[Object(' + keys.length + ' keys)]';
|
|
985
2049
|
const result = {};
|
|
986
2050
|
for (const k of keys) {
|
|
987
|
-
|
|
2051
|
+
try {
|
|
2052
|
+
result[k] = serializeValue(val[k], depth + 1);
|
|
2053
|
+
} catch {
|
|
2054
|
+
result[k] = '[Animated Value]';
|
|
2055
|
+
}
|
|
988
2056
|
}
|
|
989
2057
|
return result;
|
|
990
2058
|
}
|
|
@@ -1052,42 +2120,56 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1052
2120
|
const props = {};
|
|
1053
2121
|
for (const key of Object.keys(fiber.memoizedProps)) {
|
|
1054
2122
|
if (key === 'children') continue;
|
|
1055
|
-
|
|
2123
|
+
try {
|
|
2124
|
+
props[key] = serializeValue(fiber.memoizedProps[key]);
|
|
2125
|
+
} catch {
|
|
2126
|
+
props[key] = '[Animated Value]';
|
|
2127
|
+
}
|
|
1056
2128
|
}
|
|
1057
2129
|
result.props = props;
|
|
1058
2130
|
}
|
|
1059
2131
|
|
|
1060
2132
|
// Style separately for clarity
|
|
1061
|
-
|
|
1062
|
-
|
|
2133
|
+
try {
|
|
2134
|
+
if (fiber.memoizedProps?.style) {
|
|
2135
|
+
result.style = extractStyles(fiber.memoizedProps.style);
|
|
2136
|
+
}
|
|
2137
|
+
} catch {
|
|
2138
|
+
result.style = { _note: '[Contains animated styles]' };
|
|
1063
2139
|
}
|
|
1064
2140
|
|
|
1065
2141
|
// State (for hooks, this is a linked list)
|
|
1066
2142
|
if (includeState && fiber.memoizedState) {
|
|
1067
2143
|
// Simplified hook value serialization
|
|
1068
2144
|
function serializeHookValue(val, depth = 0) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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]'; }
|
|
1091
2173
|
}
|
|
1092
2174
|
|
|
1093
2175
|
// For function components with hooks
|
|
@@ -1126,13 +2208,13 @@ export async function inspectComponent(componentName, options = {}) {
|
|
|
1126
2208
|
return result;
|
|
1127
2209
|
})()
|
|
1128
2210
|
`;
|
|
1129
|
-
return executeInApp(expression, false);
|
|
2211
|
+
return executeInApp(expression, false, {}, device);
|
|
1130
2212
|
}
|
|
1131
2213
|
/**
|
|
1132
2214
|
* Find all components matching a name pattern and return summary info.
|
|
1133
2215
|
*/
|
|
1134
2216
|
export async function findComponents(pattern, options = {}) {
|
|
1135
|
-
const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl" } = options;
|
|
2217
|
+
const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl", device } = options;
|
|
1136
2218
|
const escapedPattern = pattern.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
|
|
1137
2219
|
const expression = `
|
|
1138
2220
|
(function() {
|
|
@@ -1173,20 +2255,25 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1173
2255
|
}
|
|
1174
2256
|
|
|
1175
2257
|
function extractLayoutStyles(style) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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; }
|
|
1190
2277
|
}
|
|
1191
2278
|
|
|
1192
2279
|
const results = [];
|
|
@@ -1194,36 +2281,47 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1194
2281
|
function search(fiber, path, depth) {
|
|
1195
2282
|
if (!fiber || results.length >= maxResults) return;
|
|
1196
2283
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
2284
|
+
try {
|
|
2285
|
+
var name = getComponentName(fiber);
|
|
2286
|
+
if (name && regex.test(name)) {
|
|
2287
|
+
var entry = {
|
|
2288
|
+
component: name,
|
|
2289
|
+
path: formatPath(path),
|
|
2290
|
+
depth
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
if (fiber.memoizedProps && fiber.memoizedProps.testID) entry.testID = fiber.memoizedProps.testID;
|
|
2294
|
+
if (fiber.key) entry.key = fiber.key;
|
|
2295
|
+
|
|
2296
|
+
if (includeLayout && fiber.memoizedProps && fiber.memoizedProps.style) {
|
|
2297
|
+
try {
|
|
2298
|
+
var layout = extractLayoutStyles(fiber.memoizedProps.style);
|
|
2299
|
+
if (layout) entry.layout = layout;
|
|
2300
|
+
} catch(e) {}
|
|
2301
|
+
}
|
|
1207
2302
|
|
|
1208
|
-
|
|
1209
|
-
const layout = extractLayoutStyles(fiber.memoizedProps.style);
|
|
1210
|
-
if (layout) entry.layout = layout;
|
|
2303
|
+
results.push(entry);
|
|
1211
2304
|
}
|
|
1212
2305
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
2306
|
+
var child = fiber.child;
|
|
2307
|
+
while (child && results.length < maxResults) {
|
|
2308
|
+
var childName = getComponentName(child);
|
|
2309
|
+
search(child, childName ? path.concat([childName]) : path, depth + 1);
|
|
2310
|
+
child = child.sibling;
|
|
2311
|
+
}
|
|
2312
|
+
} catch(e) {
|
|
2313
|
+
try {
|
|
2314
|
+
var child = fiber.child;
|
|
2315
|
+
while (child && results.length < maxResults) {
|
|
2316
|
+
search(child, path, depth + 1);
|
|
2317
|
+
child = child.sibling;
|
|
2318
|
+
}
|
|
2319
|
+
} catch(e2) {}
|
|
1221
2320
|
}
|
|
1222
2321
|
}
|
|
1223
2322
|
|
|
1224
2323
|
search(roots[0].current, [], 0);
|
|
1225
2324
|
|
|
1226
|
-
// Summary mode: just return counts by component name
|
|
1227
2325
|
if (summaryMode) {
|
|
1228
2326
|
const counts = {};
|
|
1229
2327
|
for (const r of results) {
|
|
@@ -1247,19 +2345,16 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1247
2345
|
};
|
|
1248
2346
|
})()
|
|
1249
2347
|
`;
|
|
1250
|
-
const result = await executeInApp(expression, false);
|
|
1251
|
-
// Apply TONL formatting if requested
|
|
2348
|
+
const result = await executeInApp(expression, false, {}, device);
|
|
1252
2349
|
if (format === "tonl" && result.success && result.result) {
|
|
1253
2350
|
try {
|
|
1254
2351
|
const parsed = JSON.parse(result.result);
|
|
1255
2352
|
if (parsed.components) {
|
|
1256
2353
|
if (parsed.totalMatches !== undefined) {
|
|
1257
|
-
// Summary mode
|
|
1258
2354
|
const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
|
|
1259
2355
|
return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
|
|
1260
2356
|
}
|
|
1261
2357
|
else {
|
|
1262
|
-
// Full list mode
|
|
1263
2358
|
const tonl = formatFoundComponentsToTonl(parsed.components);
|
|
1264
2359
|
return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
|
|
1265
2360
|
}
|
|
@@ -1287,19 +2382,20 @@ export async function pressElement(options) {
|
|
|
1287
2382
|
const textParam = text ? `'${esc(text)}'` : "null";
|
|
1288
2383
|
const testIDParam = testID ? `'${esc(testID)}'` : "null";
|
|
1289
2384
|
const componentParam = component ? `'${esc(component)}'` : "null";
|
|
1290
|
-
|
|
2385
|
+
// --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
|
|
2386
|
+
const dispatchExpression = `
|
|
1291
2387
|
(function() {
|
|
1292
2388
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1293
2389
|
if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
|
|
1294
2390
|
|
|
1295
2391
|
var roots = [];
|
|
1296
2392
|
if (hook.getFiberRoots) {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
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; }
|
|
1303
2399
|
}
|
|
1304
2400
|
}
|
|
1305
2401
|
if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
|
|
@@ -1308,6 +2404,7 @@ export async function pressElement(options) {
|
|
|
1308
2404
|
var searchTestID = ${testIDParam};
|
|
1309
2405
|
var searchComponent = ${componentParam};
|
|
1310
2406
|
var targetIndex = ${index};
|
|
2407
|
+
var maxTraversalUp = ${maxTraversalDepth};
|
|
1311
2408
|
|
|
1312
2409
|
function getComponentName(fiber) {
|
|
1313
2410
|
if (!fiber || !fiber.type) return null;
|
|
@@ -1316,7 +2413,7 @@ export async function pressElement(options) {
|
|
|
1316
2413
|
}
|
|
1317
2414
|
|
|
1318
2415
|
function extractText(fiber, depth) {
|
|
1319
|
-
if (!fiber || depth >
|
|
2416
|
+
if (!fiber || depth > 5000) return '';
|
|
1320
2417
|
var parts = [];
|
|
1321
2418
|
var props = fiber.memoizedProps;
|
|
1322
2419
|
if (props) {
|
|
@@ -1338,15 +2435,105 @@ export async function pressElement(options) {
|
|
|
1338
2435
|
return parts.join('');
|
|
1339
2436
|
}
|
|
1340
2437
|
|
|
1341
|
-
|
|
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
|
+
}
|
|
1342
2448
|
|
|
1343
|
-
|
|
1344
|
-
|
|
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;
|
|
1345
2533
|
var name = getComponentName(fiber);
|
|
1346
2534
|
var props = fiber.memoizedProps;
|
|
1347
2535
|
|
|
1348
|
-
|
|
1349
|
-
if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
|
|
2536
|
+
if (isScreenHidden(name, props)) return;
|
|
1350
2537
|
|
|
1351
2538
|
var isPressable = props && typeof props.onPress === 'function';
|
|
1352
2539
|
var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
|
|
@@ -1356,33 +2543,28 @@ export async function pressElement(options) {
|
|
|
1356
2543
|
if (isPressable) {
|
|
1357
2544
|
text = extractText(fiber, 0);
|
|
1358
2545
|
} else {
|
|
1359
|
-
// For inputs: use child text, value, defaultValue, or placeholder
|
|
1360
2546
|
var val = typeof props.value === 'string' ? props.value : '';
|
|
1361
2547
|
var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
|
|
1362
2548
|
var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
|
|
1363
2549
|
text = extractText(fiber, 0) || val || defVal || ph;
|
|
1364
2550
|
}
|
|
1365
2551
|
var tid = props.testID || props.nativeID || null;
|
|
1366
|
-
var
|
|
2552
|
+
var meaningful = findMeaningfulAncestorName(fiber);
|
|
2553
|
+
var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
|
|
1367
2554
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
matched = matched && (tid === searchTestID);
|
|
1373
|
-
}
|
|
1374
|
-
if (searchComponent !== null) {
|
|
1375
|
-
matched = matched && (name || '').toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
if (matched) {
|
|
1379
|
-
matches.push({
|
|
1380
|
-
fiber: fiber,
|
|
2555
|
+
var host = findFirstHost(fiber, 0, isInput);
|
|
2556
|
+
if (host) {
|
|
2557
|
+
hostFibers.push(host);
|
|
2558
|
+
tapMeta.push({
|
|
1381
2559
|
name: name || '(anonymous)',
|
|
2560
|
+
meaningfulComponentName: meaningful || null,
|
|
1382
2561
|
text: text.substring(0, 100),
|
|
1383
2562
|
testID: tid,
|
|
2563
|
+
ancestorTestIDs: ancestorIDs,
|
|
1384
2564
|
path: path.join(' > '),
|
|
1385
|
-
isInput: isInput
|
|
2565
|
+
isInput: isInput,
|
|
2566
|
+
isPressable: isPressable,
|
|
2567
|
+
source: 'direct'
|
|
1386
2568
|
});
|
|
1387
2569
|
}
|
|
1388
2570
|
}
|
|
@@ -1390,63 +2572,353 @@ export async function pressElement(options) {
|
|
|
1390
2572
|
var child = fiber.child;
|
|
1391
2573
|
while (child) {
|
|
1392
2574
|
var childName = getComponentName(child);
|
|
1393
|
-
walkFiber(child, childName ? path.concat([childName]) : path);
|
|
2575
|
+
walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1394
2576
|
child = child.sibling;
|
|
1395
2577
|
}
|
|
1396
2578
|
}
|
|
1397
2579
|
|
|
1398
2580
|
for (var ri = 0; ri < roots.length; ri++) {
|
|
1399
|
-
walkFiber(roots[ri].current, []);
|
|
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
|
+
}
|
|
2612
|
+
|
|
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
|
+
}
|
|
2672
|
+
|
|
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
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
for (var ri2a = 0; ri2a < roots.length; ri2a++) {
|
|
2705
|
+
findByTestID2a(roots[ri2a].current, []);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
1400
2708
|
}
|
|
1401
2709
|
|
|
1402
|
-
// Phase
|
|
1403
|
-
//
|
|
1404
|
-
if (
|
|
1405
|
-
var
|
|
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
|
+
}
|
|
2721
|
+
}
|
|
1406
2722
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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;
|
|
2733
|
+
}
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
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++;
|
|
2771
|
+
}
|
|
1412
2772
|
|
|
1413
|
-
|
|
1414
|
-
|
|
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;
|
|
2801
|
+
}
|
|
1415
2802
|
}
|
|
1416
|
-
var
|
|
1417
|
-
|
|
1418
|
-
var childName = getComponentName(child);
|
|
1419
|
-
findByName(child, childName ? path.concat([childName]) : path);
|
|
1420
|
-
child = child.sibling;
|
|
2803
|
+
for (var ri2b = 0; ri2b < roots.length; ri2b++) {
|
|
2804
|
+
findByName2b(roots[ri2b].current, []);
|
|
1421
2805
|
}
|
|
1422
2806
|
}
|
|
2807
|
+
}
|
|
1423
2808
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
+
}
|
|
2816
|
+
|
|
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);
|
|
2821
|
+
|
|
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;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
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; }
|
|
1449
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
|
+
});
|
|
1450
2922
|
}
|
|
1451
2923
|
}
|
|
1452
2924
|
|
|
@@ -1455,12 +2927,12 @@ export async function pressElement(options) {
|
|
|
1455
2927
|
if (searchText !== null) criteria.push('text="' + searchText + '"');
|
|
1456
2928
|
if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
|
|
1457
2929
|
if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
|
|
1458
|
-
return { error: 'No pressable or focusable elements found matching: ' + criteria.join(', ') };
|
|
2930
|
+
return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
|
|
1459
2931
|
}
|
|
1460
2932
|
|
|
1461
2933
|
if (targetIndex >= matches.length) {
|
|
1462
2934
|
return {
|
|
1463
|
-
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) + '.',
|
|
1464
2936
|
matches: matches.map(function(m, i) {
|
|
1465
2937
|
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
1466
2938
|
})
|
|
@@ -1468,50 +2940,28 @@ export async function pressElement(options) {
|
|
|
1468
2940
|
}
|
|
1469
2941
|
|
|
1470
2942
|
var target = matches[targetIndex];
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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 = {
|
|
1495
|
-
success: true,
|
|
1496
|
-
pressed: target.name,
|
|
1497
|
-
matchIndex: targetIndex,
|
|
1498
|
-
totalMatches: matches.length,
|
|
1499
|
-
text: target.text,
|
|
1500
|
-
testID: target.testID,
|
|
1501
|
-
path: target.path
|
|
1502
|
-
};
|
|
1503
|
-
if (matches.length > 1) {
|
|
1504
|
-
result.allMatches = matches.map(function(m, i) {
|
|
1505
|
-
return { index: i, component: m.name, text: m.text, testID: m.testID };
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
return result;
|
|
1509
|
-
} catch (e) {
|
|
1510
|
-
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
|
+
});
|
|
1511
2960
|
}
|
|
2961
|
+
return result;
|
|
1512
2962
|
})()
|
|
1513
2963
|
`;
|
|
1514
|
-
return executeInApp(
|
|
2964
|
+
return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, options.device);
|
|
1515
2965
|
}
|
|
1516
2966
|
// ============================================================================
|
|
1517
2967
|
// Coordinate-Based Element Inspection (via DevTools Inspector API)
|
|
@@ -1520,7 +2970,7 @@ export async function pressElement(options) {
|
|
|
1520
2970
|
* Toggle the Element Inspector via DevSettings native module.
|
|
1521
2971
|
* This enables the inspector overlay programmatically.
|
|
1522
2972
|
*/
|
|
1523
|
-
export async function toggleElementInspector() {
|
|
2973
|
+
export async function toggleElementInspector(device) {
|
|
1524
2974
|
const expression = `
|
|
1525
2975
|
(function() {
|
|
1526
2976
|
const ds = globalThis.nativeModuleProxy?.DevSettings;
|
|
@@ -1539,12 +2989,12 @@ export async function toggleElementInspector() {
|
|
|
1539
2989
|
}
|
|
1540
2990
|
})()
|
|
1541
2991
|
`;
|
|
1542
|
-
return executeInApp(expression, false);
|
|
2992
|
+
return executeInApp(expression, false, {}, device);
|
|
1543
2993
|
}
|
|
1544
2994
|
/**
|
|
1545
2995
|
* Check if the Element Inspector overlay is currently active.
|
|
1546
2996
|
*/
|
|
1547
|
-
export async function isInspectorActive() {
|
|
2997
|
+
export async function isInspectorActive(device) {
|
|
1548
2998
|
const expression = `
|
|
1549
2999
|
(function() {
|
|
1550
3000
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -1560,7 +3010,7 @@ export async function isInspectorActive() {
|
|
|
1560
3010
|
if (roots.length === 0) return false;
|
|
1561
3011
|
|
|
1562
3012
|
function findComponent(fiber, targetName, depth = 0) {
|
|
1563
|
-
if (!fiber || depth >
|
|
3013
|
+
if (!fiber || depth > 5000) return null;
|
|
1564
3014
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1565
3015
|
if (name === targetName) return fiber;
|
|
1566
3016
|
let child = fiber.child;
|
|
@@ -1575,7 +3025,7 @@ export async function isInspectorActive() {
|
|
|
1575
3025
|
return !!findComponent(roots[0].current, 'InspectorPanel');
|
|
1576
3026
|
})()
|
|
1577
3027
|
`;
|
|
1578
|
-
const result = await executeInApp(expression, false);
|
|
3028
|
+
const result = await executeInApp(expression, false, {}, device);
|
|
1579
3029
|
if (result.success && result.result) {
|
|
1580
3030
|
return result.result === "true";
|
|
1581
3031
|
}
|
|
@@ -1586,7 +3036,7 @@ export async function isInspectorActive() {
|
|
|
1586
3036
|
* This reads the InspectorPanel component's props to get the hierarchy, frame, and style.
|
|
1587
3037
|
* Requires the Element Inspector to be enabled and an element to be selected.
|
|
1588
3038
|
*/
|
|
1589
|
-
export async function getInspectorSelection() {
|
|
3039
|
+
export async function getInspectorSelection(device) {
|
|
1590
3040
|
const expression = `
|
|
1591
3041
|
(function() {
|
|
1592
3042
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -1602,28 +3052,29 @@ export async function getInspectorSelection() {
|
|
|
1602
3052
|
}
|
|
1603
3053
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
1604
3054
|
|
|
1605
|
-
// Find InspectorPanel
|
|
1606
|
-
function
|
|
1607
|
-
if (!fiber || depth >
|
|
3055
|
+
// Find all InspectorPanel instances (apps with modals may have multiple)
|
|
3056
|
+
function findAllPanels(fiber, targetName, depth, results) {
|
|
3057
|
+
if (!fiber || depth > 5000) return;
|
|
1608
3058
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1609
|
-
if (name === targetName)
|
|
3059
|
+
if (name === targetName) results.push(fiber);
|
|
1610
3060
|
let child = fiber.child;
|
|
1611
3061
|
while (child) {
|
|
1612
|
-
|
|
1613
|
-
if (found) return found;
|
|
3062
|
+
findAllPanels(child, targetName, depth + 1, results);
|
|
1614
3063
|
child = child.sibling;
|
|
1615
3064
|
}
|
|
1616
|
-
return null;
|
|
1617
3065
|
}
|
|
1618
3066
|
|
|
1619
|
-
const
|
|
1620
|
-
|
|
3067
|
+
const panels = [];
|
|
3068
|
+
findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
|
|
3069
|
+
if (panels.length === 0) {
|
|
1621
3070
|
return {
|
|
1622
3071
|
error: 'Element Inspector is not active.',
|
|
1623
3072
|
hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
|
|
1624
3073
|
};
|
|
1625
3074
|
}
|
|
1626
3075
|
|
|
3076
|
+
// Prefer the panel that has an active selection
|
|
3077
|
+
const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
|
|
1627
3078
|
const props = panelFiber.memoizedProps;
|
|
1628
3079
|
if (!props.hierarchy || props.hierarchy.length === 0) {
|
|
1629
3080
|
return {
|
|
@@ -1659,7 +3110,7 @@ export async function getInspectorSelection() {
|
|
|
1659
3110
|
};
|
|
1660
3111
|
})()
|
|
1661
3112
|
`;
|
|
1662
|
-
return executeInApp(expression, false);
|
|
3113
|
+
return executeInApp(expression, false, {}, device);
|
|
1663
3114
|
}
|
|
1664
3115
|
/**
|
|
1665
3116
|
* Inspect the React component at a specific (x, y) coordinate.
|
|
@@ -1674,7 +3125,7 @@ export async function getInspectorSelection() {
|
|
|
1674
3125
|
* coordinates, return the innermost matching React component.
|
|
1675
3126
|
*/
|
|
1676
3127
|
export async function inspectAtPoint(x, y, options = {}) {
|
|
1677
|
-
const { includeProps = true, includeFrame = true } = options;
|
|
3128
|
+
const { includeProps = true, includeFrame = true, device } = options;
|
|
1678
3129
|
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
1679
3130
|
const dispatchExpression = `
|
|
1680
3131
|
(function() {
|
|
@@ -1736,7 +3187,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1736
3187
|
return { count: hostFibers.length };
|
|
1737
3188
|
})()
|
|
1738
3189
|
`;
|
|
1739
|
-
const dispatchResult = await executeInApp(dispatchExpression, false);
|
|
3190
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, {}, device);
|
|
1740
3191
|
if (!dispatchResult.success)
|
|
1741
3192
|
return dispatchResult;
|
|
1742
3193
|
try {
|
|
@@ -1782,7 +3233,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1782
3233
|
|
|
1783
3234
|
// RN primitives and internal components to skip when surfacing the "element" name.
|
|
1784
3235
|
// 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|
|
|
3236
|
+
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
3237
|
|
|
1787
3238
|
function getNearestNamed(fiber, skipPrimitives) {
|
|
1788
3239
|
var cur = fiber;
|
|
@@ -1872,6 +3323,6 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
1872
3323
|
return result;
|
|
1873
3324
|
})()
|
|
1874
3325
|
`;
|
|
1875
|
-
return executeInApp(resolveExpression, false);
|
|
3326
|
+
return executeInApp(resolveExpression, false, {}, device);
|
|
1876
3327
|
}
|
|
1877
3328
|
//# sourceMappingURL=executor.js.map
|