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.
Files changed (168) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +386 -45
  3. package/build/__tests__/helpers/tap-test-helpers.d.ts +36 -0
  4. package/build/__tests__/helpers/tap-test-helpers.d.ts.map +1 -0
  5. package/build/__tests__/helpers/tap-test-helpers.js +130 -0
  6. package/build/__tests__/helpers/tap-test-helpers.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.js +12 -6
  8. package/build/__tests__/integration/connection-health.test.js.map +1 -1
  9. package/build/__tests__/integration/execute-in-app.test.js +2 -1
  10. package/build/__tests__/integration/execute-in-app.test.js.map +1 -1
  11. package/build/__tests__/integration/tap-strategies.test.d.ts +2 -0
  12. package/build/__tests__/integration/tap-strategies.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/tap-strategies.test.js +329 -0
  14. package/build/__tests__/integration/tap-strategies.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.js +9 -10
  16. package/build/__tests__/integration/tools.test.js.map +1 -1
  17. package/build/__tests__/unit/burst.test.d.ts +2 -0
  18. package/build/__tests__/unit/burst.test.d.ts.map +1 -0
  19. package/build/__tests__/unit/burst.test.js +50 -0
  20. package/build/__tests__/unit/burst.test.js.map +1 -0
  21. package/build/__tests__/unit/connection-health.test.js +26 -16
  22. package/build/__tests__/unit/connection-health.test.js.map +1 -1
  23. package/build/__tests__/unit/fiberMatch.test.d.ts +2 -0
  24. package/build/__tests__/unit/fiberMatch.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/fiberMatch.test.js +114 -0
  26. package/build/__tests__/unit/fiberMatch.test.js.map +1 -0
  27. package/build/__tests__/unit/fingerprint.test.d.ts +2 -0
  28. package/build/__tests__/unit/fingerprint.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/fingerprint.test.js +89 -0
  30. package/build/__tests__/unit/fingerprint.test.js.map +1 -0
  31. package/build/__tests__/unit/imageBuffer.test.d.ts +2 -0
  32. package/build/__tests__/unit/imageBuffer.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/imageBuffer.test.js +128 -0
  34. package/build/__tests__/unit/imageBuffer.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.js +9 -0
  36. package/build/__tests__/unit/logs.test.js.map +1 -1
  37. package/build/__tests__/unit/networkInterceptor.test.d.ts +2 -0
  38. package/build/__tests__/unit/networkInterceptor.test.d.ts.map +1 -0
  39. package/build/__tests__/unit/networkInterceptor.test.js +104 -0
  40. package/build/__tests__/unit/networkInterceptor.test.js.map +1 -0
  41. package/build/__tests__/unit/screenshot-diff.test.d.ts +2 -0
  42. package/build/__tests__/unit/screenshot-diff.test.d.ts.map +1 -0
  43. package/build/__tests__/unit/screenshot-diff.test.js +97 -0
  44. package/build/__tests__/unit/screenshot-diff.test.js.map +1 -0
  45. package/build/__tests__/unit/tap.test.js +542 -25
  46. package/build/__tests__/unit/tap.test.js.map +1 -1
  47. package/build/core/android.d.ts.map +1 -1
  48. package/build/core/android.js +104 -113
  49. package/build/core/android.js.map +1 -1
  50. package/build/core/appDetection.d.ts +8 -0
  51. package/build/core/appDetection.d.ts.map +1 -0
  52. package/build/core/appDetection.js +115 -0
  53. package/build/core/appDetection.js.map +1 -0
  54. package/build/core/bundle.d.ts.map +1 -1
  55. package/build/core/bundle.js +15 -23
  56. package/build/core/bundle.js.map +1 -1
  57. package/build/core/config.d.ts +8 -0
  58. package/build/core/config.d.ts.map +1 -0
  59. package/build/core/config.js +27 -0
  60. package/build/core/config.js.map +1 -0
  61. package/build/core/connection.d.ts +47 -3
  62. package/build/core/connection.d.ts.map +1 -1
  63. package/build/core/connection.js +519 -124
  64. package/build/core/connection.js.map +1 -1
  65. package/build/core/executor.d.ts +82 -14
  66. package/build/core/executor.d.ts.map +1 -1
  67. package/build/core/executor.js +1869 -418
  68. package/build/core/executor.js.map +1 -1
  69. package/build/core/feedback.d.ts +18 -0
  70. package/build/core/feedback.d.ts.map +1 -0
  71. package/build/core/feedback.js +73 -0
  72. package/build/core/feedback.js.map +1 -0
  73. package/build/core/fiberMatch.d.ts +25 -0
  74. package/build/core/fiberMatch.d.ts.map +1 -0
  75. package/build/core/fiberMatch.js +35 -0
  76. package/build/core/fiberMatch.js.map +1 -0
  77. package/build/core/fingerprint.d.ts +4 -0
  78. package/build/core/fingerprint.d.ts.map +1 -0
  79. package/build/core/fingerprint.js +47 -0
  80. package/build/core/fingerprint.js.map +1 -0
  81. package/build/core/guides.d.ts.map +1 -1
  82. package/build/core/guides.js +66 -3
  83. package/build/core/guides.js.map +1 -1
  84. package/build/core/imageBuffer.d.ts +37 -0
  85. package/build/core/imageBuffer.d.ts.map +1 -0
  86. package/build/core/imageBuffer.js +58 -0
  87. package/build/core/imageBuffer.js.map +1 -0
  88. package/build/core/index.d.ts +22 -17
  89. package/build/core/index.d.ts.map +1 -1
  90. package/build/core/index.js +23 -19
  91. package/build/core/index.js.map +1 -1
  92. package/build/core/ios.d.ts +36 -7
  93. package/build/core/ios.d.ts.map +1 -1
  94. package/build/core/ios.js +561 -326
  95. package/build/core/ios.js.map +1 -1
  96. package/build/core/license.d.ts +38 -0
  97. package/build/core/license.d.ts.map +1 -0
  98. package/build/core/license.js +243 -0
  99. package/build/core/license.js.map +1 -0
  100. package/build/core/logbox.d.ts +57 -0
  101. package/build/core/logbox.d.ts.map +1 -0
  102. package/build/core/logbox.js +282 -0
  103. package/build/core/logbox.js.map +1 -0
  104. package/build/core/logs.d.ts +1 -0
  105. package/build/core/logs.d.ts.map +1 -1
  106. package/build/core/logs.js +5 -0
  107. package/build/core/logs.js.map +1 -1
  108. package/build/core/metro.d.ts +8 -0
  109. package/build/core/metro.d.ts.map +1 -1
  110. package/build/core/metro.js +28 -0
  111. package/build/core/metro.js.map +1 -1
  112. package/build/core/networkInterceptor.d.ts +29 -2
  113. package/build/core/networkInterceptor.d.ts.map +1 -1
  114. package/build/core/networkInterceptor.js +195 -191
  115. package/build/core/networkInterceptor.js.map +1 -1
  116. package/build/core/ocr.d.ts +8 -3
  117. package/build/core/ocr.d.ts.map +1 -1
  118. package/build/core/ocr.js +116 -15
  119. package/build/core/ocr.js.map +1 -1
  120. package/build/core/posthog.d.ts +5 -0
  121. package/build/core/posthog.d.ts.map +1 -0
  122. package/build/core/posthog.js +46 -0
  123. package/build/core/posthog.js.map +1 -0
  124. package/build/core/sdkBridge.d.ts +67 -0
  125. package/build/core/sdkBridge.d.ts.map +1 -0
  126. package/build/core/sdkBridge.js +148 -0
  127. package/build/core/sdkBridge.js.map +1 -0
  128. package/build/core/state.d.ts +25 -5
  129. package/build/core/state.d.ts.map +1 -1
  130. package/build/core/state.js +79 -10
  131. package/build/core/state.js.map +1 -1
  132. package/build/core/telemetry.d.ts +21 -1
  133. package/build/core/telemetry.d.ts.map +1 -1
  134. package/build/core/telemetry.js +166 -10
  135. package/build/core/telemetry.js.map +1 -1
  136. package/build/core/types.d.ts +11 -0
  137. package/build/core/types.d.ts.map +1 -1
  138. package/build/index.js +1490 -324
  139. package/build/index.js.map +1 -1
  140. package/build/pro/screenshot-diff.d.ts +10 -0
  141. package/build/pro/screenshot-diff.d.ts.map +1 -0
  142. package/build/pro/screenshot-diff.js +38 -0
  143. package/build/pro/screenshot-diff.js.map +1 -0
  144. package/build/pro/tap.d.ts +84 -2
  145. package/build/pro/tap.d.ts.map +1 -1
  146. package/build/pro/tap.js +835 -129
  147. package/build/pro/tap.js.map +1 -1
  148. package/build/tools/accountTools.d.ts +44 -0
  149. package/build/tools/accountTools.d.ts.map +1 -0
  150. package/build/tools/accountTools.js +217 -0
  151. package/build/tools/accountTools.js.map +1 -0
  152. package/package.json +8 -7
  153. package/build/core/httpServer.d.ts +0 -14
  154. package/build/core/httpServer.d.ts.map +0 -1
  155. package/build/core/httpServer.js +0 -2459
  156. package/build/core/httpServer.js.map +0 -1
  157. package/build/core/httpServerProcess.d.ts +0 -25
  158. package/build/core/httpServerProcess.d.ts.map +0 -1
  159. package/build/core/httpServerProcess.js +0 -153
  160. package/build/core/httpServerProcess.js.map +0 -1
  161. package/build/core/tap.d.ts +0 -91
  162. package/build/core/tap.d.ts.map +0 -1
  163. package/build/core/tap.js +0 -542
  164. package/build/core/tap.js.map +0 -1
  165. package/build/httpServerStandalone.d.ts +0 -7
  166. package/build/httpServerStandalone.d.ts.map +0 -1
  167. package/build/httpServerStandalone.js +0 -31
  168. package/build/httpServerStandalone.js.map +0 -1
@@ -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
- resolve({ success: false, error: "Timeout: Expression took too long to evaluate" });
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 = getFirstConnectedApp();
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 = getFirstConnectedApp();
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 ?? "Execution failed after all retries. Connection may be stale."
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 (Page.reload is not supported by Hermes)
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 = getFirstConnectedApp();
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 = getFirstConnectedApp();
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
- return { success: false, error: "WebSocket connection is not open." };
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
- // Close existing connections to this port and cancel any pending auto-reconnections
395
- // This prevents the dual-reconnection bug where both auto-reconnect and manual reconnect compete
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.port === port) {
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 to Metro on the same port with auto-reconnection DISABLED
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 mainDevice = selectMainDevice(devices);
416
- if (mainDevice) {
417
- await connectToDevice(mainDevice, port, {
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 ${mainDevice.title}`
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
- function formatScreenLayoutToTonl(elements) {
478
- const lines = ["#elements{component,path,depth,layout,id}"];
479
- for (const el of elements) {
480
- const layout = el.layout
481
- ? Object.entries(el.layout)
482
- .map(([k, v]) => `${k}:${v}`)
483
- .join(";")
484
- : "";
485
- const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
486
- lines.push(`${el.component}|${el.path}|${el.depth}|${layout}|${id}`);
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 ?? (structureOnly ? (focusedOnly ? 25 : 40) : 100);
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
- if (!style) return null;
580
- const merged = Array.isArray(style)
581
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
582
- : (typeof style === 'object' ? style : {});
583
-
584
- const layout = {};
585
- const layoutKeys = [
586
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
587
- 'paddingHorizontal', 'paddingVertical',
588
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
589
- 'marginHorizontal', 'marginVertical',
590
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
591
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
592
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
593
- 'position', 'top', 'bottom', 'left', 'right',
594
- 'gap', 'rowGap', 'columnGap',
595
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
596
- ];
597
-
598
- for (const key of layoutKeys) {
599
- if (merged[key] !== undefined) layout[key] = merged[key];
600
- }
601
- return Object.keys(layout).length > 0 ? layout : null;
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
- const val = fiber.memoizedProps[key];
631
- if (typeof val === 'function') {
632
- props[key] = '[Function]';
633
- } else if (typeof val === 'object' && val !== null) {
634
- props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
635
- } else {
636
- props[key] = val;
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
- if (includeStyles && fiber.memoizedProps?.style) {
644
- const layout = extractLayoutStyles(fiber.memoizedProps.style);
645
- if (layout) node.layout = layout;
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 > 200) return null;
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 styles for all components on the current screen.
740
- * Useful for verifying layout without screenshots.
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 { maxDepth = 65, componentsOnly = false, shortPath = true, summary = false, format = "tonl" } = options;
744
- const expression = `
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
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1055
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
747
1056
  if (!hook) return { error: 'React DevTools hook not found.' };
748
1057
 
749
- let roots = [];
1058
+ var roots = [];
750
1059
  if (hook.getFiberRoots) {
751
- roots = [...(hook.getFiberRoots(1) || [])];
1060
+ roots = Array.from(hook.getFiberRoots(1) || []);
752
1061
  }
753
1062
  if (roots.length === 0 && hook.renderers) {
754
- for (const [id] of hook.renderers) {
755
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
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
- const maxDepth = ${maxDepth};
762
- const componentsOnly = ${componentsOnly};
763
- const shortPath = ${shortPath};
764
- const summaryMode = ${summary};
765
- const pathSegments = 3; // Number of path segments to show in shortPath mode
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
- function isHostComponent(fiber) {
774
- return typeof fiber?.type === 'string';
775
- }
776
-
777
- function formatPath(pathArray) {
778
- if (!shortPath || pathArray.length <= pathSegments) {
779
- return pathArray.join(' > ');
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 '... > ' + pathArray.slice(-pathSegments).join(' > ');
1107
+ return null;
782
1108
  }
783
1109
 
784
- function extractAllStyles(style) {
785
- if (!style) return null;
786
- const merged = Array.isArray(style)
787
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
788
- : (typeof style === 'object' ? style : {});
789
- return Object.keys(merged).length > 0 ? merged : null;
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
- function extractLayoutStyles(style) {
793
- if (!style) return null;
794
- const merged = Array.isArray(style)
795
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
796
- : (typeof style === 'object' ? style : {});
797
-
798
- const layout = {};
799
- const layoutKeys = [
800
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
801
- 'paddingHorizontal', 'paddingVertical',
802
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
803
- 'marginHorizontal', 'marginVertical',
804
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
805
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
806
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
807
- 'position', 'top', 'bottom', 'left', 'right',
808
- 'gap', 'rowGap', 'columnGap',
809
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
810
- 'backgroundColor', 'borderColor', 'borderRadius'
811
- ];
812
-
813
- for (const key of layoutKeys) {
814
- if (merged[key] !== undefined) layout[key] = merged[key];
815
- }
816
- return Object.keys(layout).length > 0 ? layout : null;
817
- }
818
-
819
- const elements = [];
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
- function walkFiber(fiber, depth, path) {
822
- if (!fiber || depth > maxDepth) return;
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
- const name = getComponentName(fiber);
825
- const isHost = isHostComponent(fiber);
826
-
827
- // Include host components (View, Text, etc.) or named components
828
- if (name && (!componentsOnly || !isHost)) {
829
- const style = fiber.memoizedProps?.style;
830
- const layout = extractLayoutStyles(style);
831
-
832
- // Get text content if it's a Text component
833
- let textContent = null;
834
- if (name === 'Text' || name === 'RCTText') {
835
- const children = fiber.memoizedProps?.children;
836
- if (typeof children === 'string') textContent = children;
837
- else if (typeof children === 'number') textContent = String(children);
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
- const element = {
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
- if (layout) element.layout = layout;
847
- if (textContent) element.text = textContent.slice(0, 100);
848
-
849
- // Include key props for identification
850
- if (fiber.memoizedProps) {
851
- const identifiers = {};
852
- if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
853
- if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
854
- if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
855
- if (fiber.key) identifiers.key = fiber.key;
856
- if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
857
- }
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
- elements.push(element);
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
- // Traverse children
863
- let child = fiber.child;
864
- while (child) {
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
- walkFiber(roots[0].current, 0, []);
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
- const counts = {};
876
- for (const el of elements) {
877
- counts[el.component] = (counts[el.component] || 0) + 1;
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
- // Sort by count descending
880
- const sorted = Object.entries(counts)
881
- .sort((a, b) => b[1] - a[1])
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
- // Use a longer timeout for layout traversal — large component trees can exceed 10s
897
- const result = await executeInApp(expression, false, { timeoutMs: 30000 });
898
- // Apply TONL formatting if requested
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
- // Full element list
909
- const tonl = formatScreenLayoutToTonl(parsed.elements);
910
- return { success: true, result: tonl };
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
- * Inspect a specific component by name, returning its props, state, and layout.
921
- */
922
- export async function inspectComponent(componentName, options = {}) {
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
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1488
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
928
1489
  if (!hook) return { error: 'React DevTools hook not found.' };
929
1490
 
930
- let roots = [];
1491
+ var roots = [];
931
1492
  if (hook.getFiberRoots) {
932
- roots = [...(hook.getFiberRoots(1) || [])];
1493
+ roots = Array.from(hook.getFiberRoots(1) || []);
933
1494
  }
934
1495
  if (roots.length === 0 && hook.renderers) {
935
- for (const [id] of hook.renderers) {
936
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
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
- const targetName = '${escapedName}';
943
- const targetIndex = ${index};
944
- const includeState = ${includeState};
945
- const includeChildren = ${includeChildren};
946
- const childrenDepth = ${childrenDepth};
947
- const shortPath = ${shortPath};
948
- const simplifyHooks = ${simplifyHooks};
949
- const pathSegments = 3;
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 getComponentName(fiber) {
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
- if (!style) return null;
966
- const merged = Array.isArray(style)
967
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
968
- : (typeof style === 'object' ? style : {});
969
- return Object.keys(merged).length > 0 ? merged : null;
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
- result[k] = serializeValue(val[k], depth + 1);
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
- props[key] = serializeValue(fiber.memoizedProps[key]);
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
- if (fiber.memoizedProps?.style) {
1062
- result.style = extractStyles(fiber.memoizedProps.style);
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
- if (depth > 2) return '[...]';
1070
- if (val === null || val === undefined) return val;
1071
- if (typeof val === 'function') return '[Function]';
1072
- if (typeof val !== 'object') return val;
1073
- // Skip React internal structures (effects, refs with destroy/create)
1074
- if (val.create && val.destroy !== undefined) return '[Effect]';
1075
- if (val.inst && val.deps) return '[Effect]';
1076
- if (val.current !== undefined && Object.keys(val).length === 1) {
1077
- // Ref object - just show current value
1078
- return { current: serializeHookValue(val.current, depth + 1) };
1079
- }
1080
- if (Array.isArray(val)) {
1081
- if (val.length > 5) return '[Array(' + val.length + ')]';
1082
- return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
1083
- }
1084
- const keys = Object.keys(val);
1085
- if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
1086
- const result = {};
1087
- for (const k of keys.slice(0, 10)) {
1088
- result[k] = serializeHookValue(val[k], depth + 1);
1089
- }
1090
- return result;
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
- if (!style) return null;
1177
- const merged = Array.isArray(style)
1178
- ? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
1179
- : (typeof style === 'object' ? style : {});
1180
-
1181
- const layout = {};
1182
- const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1183
- 'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
1184
- 'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
1185
- 'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
1186
- for (const k of keys) {
1187
- if (merged[k] !== undefined) layout[k] = merged[k];
1188
- }
1189
- return Object.keys(layout).length > 0 ? layout : null;
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
- const name = getComponentName(fiber);
1198
- if (name && regex.test(name)) {
1199
- const entry = {
1200
- component: name,
1201
- path: formatPath(path),
1202
- depth
1203
- };
1204
-
1205
- if (fiber.memoizedProps?.testID) entry.testID = fiber.memoizedProps.testID;
1206
- if (fiber.key) entry.key = fiber.key;
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
- if (includeLayout && fiber.memoizedProps?.style) {
1209
- const layout = extractLayoutStyles(fiber.memoizedProps.style);
1210
- if (layout) entry.layout = layout;
2303
+ results.push(entry);
1211
2304
  }
1212
2305
 
1213
- results.push(entry);
1214
- }
1215
-
1216
- let child = fiber.child;
1217
- while (child && results.length < maxResults) {
1218
- const childName = getComponentName(child);
1219
- search(child, childName ? [...path, childName] : path, depth + 1);
1220
- child = child.sibling;
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
- const expression = `
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
- var renderers = hook.renderers;
1298
- if (renderers) {
1299
- for (var entry of renderers) {
1300
- var r = Array.from(hook.getFiberRoots(entry[0]) || []);
1301
- if (r.length > 0) { roots = r; break; }
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 > 15) return '';
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
- var matches = [];
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
- function walkFiber(fiber, path) {
1344
- if (!fiber) return;
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
- // Skip off-screen subtrees — React Navigation marks hidden screens with aria-hidden
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 matched = true;
2552
+ var meaningful = findMeaningfulAncestorName(fiber);
2553
+ var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
1367
2554
 
1368
- if (searchText !== null) {
1369
- matched = matched && text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
1370
- }
1371
- if (searchTestID !== null) {
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 2: If searching by component and no match found,
1403
- // look for the component by name and walk UP to find nearest pressable ancestor
1404
- if (matches.length === 0 && searchComponent !== null) {
1405
- var componentCandidates = [];
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
- function findByName(fiber, path) {
1408
- if (!fiber) return;
1409
- var name = getComponentName(fiber);
1410
- var props = fiber.memoizedProps;
1411
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
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
- if (name && name.toLowerCase().indexOf(searchComponent.toLowerCase()) !== -1) {
1414
- componentCandidates.push({ fiber: fiber, name: name, path: path.join(' > ') });
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 child = fiber.child;
1417
- while (child) {
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
- for (var ri2 = 0; ri2 < roots.length; ri2++) {
1425
- findByName(roots[ri2].current, []);
1426
- }
1427
-
1428
- // For each candidate, walk up to find pressable ancestor
1429
- var maxDepth = ${maxTraversalDepth};
1430
- for (var ci = 0; ci < componentCandidates.length; ci++) {
1431
- var candidate = componentCandidates[ci];
1432
- var parent = candidate.fiber.return;
1433
- var depth = 0;
1434
- while (parent && depth < maxDepth) {
1435
- var parentProps = parent.memoizedProps;
1436
- if (parentProps && typeof parentProps.onPress === 'function') {
1437
- var text = extractText(parent, 0);
1438
- matches.push({
1439
- fiber: parent,
1440
- name: candidate.name,
1441
- text: text.substring(0, 100),
1442
- testID: parentProps.testID || parentProps.nativeID || null,
1443
- path: candidate.path
1444
- });
1445
- break;
1446
- }
1447
- parent = parent.return;
1448
- depth++;
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
- try {
1472
- if (target.isInput) {
1473
- // Input elements can't be pressed via fiber — return match info
1474
- // so the orchestrator can fall through to accessibility/coordinate tap
1475
- var result = {
1476
- success: true,
1477
- pressed: target.name,
1478
- matchIndex: targetIndex,
1479
- totalMatches: matches.length,
1480
- text: target.text,
1481
- testID: target.testID,
1482
- path: target.path,
1483
- isInput: true,
1484
- needsNativeTap: true
1485
- };
1486
- if (matches.length > 1) {
1487
- result.allMatches = matches.map(function(m, i) {
1488
- return { index: i, component: m.name, text: m.text, testID: m.testID };
1489
- });
1490
- }
1491
- return result;
1492
- }
1493
- target.fiber.memoizedProps.onPress();
1494
- var result = {
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(expression, false);
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 > 100) return null;
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 component
1606
- function findComponent(fiber, targetName, depth = 0) {
1607
- if (!fiber || depth > 100) return null;
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) return fiber;
3059
+ if (name === targetName) results.push(fiber);
1610
3060
  let child = fiber.child;
1611
3061
  while (child) {
1612
- const found = findComponent(child, targetName, depth + 1);
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 panelFiber = findComponent(roots[0].current, 'InspectorPanel');
1620
- if (!panelFiber) {
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|Modal|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.*)$/;
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