react-native-ai-debugger 1.6.16 → 1.8.1

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 (160) hide show
  1. package/README.md +8 -1
  2. package/build/__tests__/unit/exec.test.d.ts +2 -0
  3. package/build/__tests__/unit/exec.test.d.ts.map +1 -0
  4. package/build/__tests__/unit/exec.test.js +54 -0
  5. package/build/__tests__/unit/exec.test.js.map +1 -0
  6. package/build/__tests__/unit/focusedInput.test.d.ts +2 -0
  7. package/build/__tests__/unit/focusedInput.test.d.ts.map +1 -0
  8. package/build/__tests__/unit/focusedInput.test.js +58 -0
  9. package/build/__tests__/unit/focusedInput.test.js.map +1 -0
  10. package/build/__tests__/unit/focusedInputTools.test.d.ts +2 -0
  11. package/build/__tests__/unit/focusedInputTools.test.d.ts.map +1 -0
  12. package/build/__tests__/unit/focusedInputTools.test.js +79 -0
  13. package/build/__tests__/unit/focusedInputTools.test.js.map +1 -0
  14. package/build/__tests__/unit/tap.test.js +46 -0
  15. package/build/__tests__/unit/tap.test.js.map +1 -1
  16. package/build/core/android.d.ts +3 -3
  17. package/build/core/android.d.ts.map +1 -1
  18. package/build/core/android.js +21 -14
  19. package/build/core/android.js.map +1 -1
  20. package/build/core/appDetection.d.ts +7 -0
  21. package/build/core/appDetection.d.ts.map +1 -1
  22. package/build/core/appDetection.js +38 -17
  23. package/build/core/appDetection.js.map +1 -1
  24. package/build/core/componentSearch.d.ts +38 -0
  25. package/build/core/componentSearch.d.ts.map +1 -0
  26. package/build/core/componentSearch.js +419 -0
  27. package/build/core/componentSearch.js.map +1 -0
  28. package/build/core/componentTree.d.ts +12 -0
  29. package/build/core/componentTree.d.ts.map +1 -0
  30. package/build/core/componentTree.js +275 -0
  31. package/build/core/componentTree.js.map +1 -0
  32. package/build/core/connection.d.ts +1 -0
  33. package/build/core/connection.d.ts.map +1 -1
  34. package/build/core/connection.js +99 -89
  35. package/build/core/connection.js.map +1 -1
  36. package/build/core/debugGlobals.d.ts +5 -0
  37. package/build/core/debugGlobals.d.ts.map +1 -0
  38. package/build/core/debugGlobals.js +300 -0
  39. package/build/core/debugGlobals.js.map +1 -0
  40. package/build/core/errors.d.ts +14 -0
  41. package/build/core/errors.d.ts.map +1 -0
  42. package/build/core/errors.js +17 -0
  43. package/build/core/errors.js.map +1 -0
  44. package/build/core/exec.d.ts +22 -0
  45. package/build/core/exec.d.ts.map +1 -0
  46. package/build/core/exec.js +54 -0
  47. package/build/core/exec.js.map +1 -0
  48. package/build/core/executor.d.ts +7 -204
  49. package/build/core/executor.d.ts.map +1 -1
  50. package/build/core/executor.js +10 -4103
  51. package/build/core/executor.js.map +1 -1
  52. package/build/core/focusedInput.d.ts +27 -0
  53. package/build/core/focusedInput.d.ts.map +1 -0
  54. package/build/core/focusedInput.js +110 -0
  55. package/build/core/focusedInput.js.map +1 -0
  56. package/build/core/focusedInputTools.d.ts +21 -0
  57. package/build/core/focusedInputTools.d.ts.map +1 -0
  58. package/build/core/focusedInputTools.js +51 -0
  59. package/build/core/focusedInputTools.js.map +1 -0
  60. package/build/core/guides.d.ts.map +1 -1
  61. package/build/core/guides.js +15 -2
  62. package/build/core/guides.js.map +1 -1
  63. package/build/core/index.d.ts +4 -3
  64. package/build/core/index.d.ts.map +1 -1
  65. package/build/core/index.js +4 -3
  66. package/build/core/index.js.map +1 -1
  67. package/build/core/inspector.d.ts +50 -0
  68. package/build/core/inspector.d.ts.map +1 -0
  69. package/build/core/inspector.js +604 -0
  70. package/build/core/inspector.js.map +1 -0
  71. package/build/core/ios.d.ts +8 -0
  72. package/build/core/ios.d.ts.map +1 -1
  73. package/build/core/ios.js +142 -2
  74. package/build/core/ios.js.map +1 -1
  75. package/build/core/jsExecute.d.ts +27 -0
  76. package/build/core/jsExecute.d.ts.map +1 -0
  77. package/build/core/jsExecute.js +453 -0
  78. package/build/core/jsExecute.js.map +1 -0
  79. package/build/core/ocr.d.ts +13 -0
  80. package/build/core/ocr.d.ts.map +1 -1
  81. package/build/core/ocr.js +37 -15
  82. package/build/core/ocr.js.map +1 -1
  83. package/build/core/posthog.d.ts.map +1 -1
  84. package/build/core/posthog.js +7 -1
  85. package/build/core/posthog.js.map +1 -1
  86. package/build/core/pressables.d.ts +44 -0
  87. package/build/core/pressables.d.ts.map +1 -0
  88. package/build/core/pressables.js +1480 -0
  89. package/build/core/pressables.js.map +1 -0
  90. package/build/core/register.d.ts +8 -0
  91. package/build/core/register.d.ts.map +1 -0
  92. package/build/core/register.js +216 -0
  93. package/build/core/register.js.map +1 -0
  94. package/build/core/screenLayout.d.ts +52 -0
  95. package/build/core/screenLayout.d.ts.map +1 -0
  96. package/build/core/screenLayout.js +828 -0
  97. package/build/core/screenLayout.js.map +1 -0
  98. package/build/core/toolHelpers.d.ts +24 -0
  99. package/build/core/toolHelpers.d.ts.map +1 -0
  100. package/build/core/toolHelpers.js +118 -0
  101. package/build/core/toolHelpers.js.map +1 -0
  102. package/build/core/types.d.ts +11 -6
  103. package/build/core/types.d.ts.map +1 -1
  104. package/build/index.d.ts +2 -4
  105. package/build/index.d.ts.map +1 -1
  106. package/build/index.js +37 -4313
  107. package/build/index.js.map +1 -1
  108. package/build/pro/tap.d.ts +51 -5
  109. package/build/pro/tap.d.ts.map +1 -1
  110. package/build/pro/tap.js +457 -54
  111. package/build/pro/tap.js.map +1 -1
  112. package/build/tools/accountTools.d.ts +2 -0
  113. package/build/tools/accountTools.d.ts.map +1 -1
  114. package/build/tools/accountTools.js +5 -0
  115. package/build/tools/accountTools.js.map +1 -1
  116. package/build/tools/bundleTools.d.ts +3 -0
  117. package/build/tools/bundleTools.d.ts.map +1 -0
  118. package/build/tools/bundleTools.js +368 -0
  119. package/build/tools/bundleTools.js.map +1 -0
  120. package/build/tools/componentTools.d.ts +3 -0
  121. package/build/tools/componentTools.d.ts.map +1 -0
  122. package/build/tools/componentTools.js +639 -0
  123. package/build/tools/componentTools.js.map +1 -0
  124. package/build/tools/connectionTools.d.ts +3 -0
  125. package/build/tools/connectionTools.d.ts.map +1 -0
  126. package/build/tools/connectionTools.js +564 -0
  127. package/build/tools/connectionTools.js.map +1 -0
  128. package/build/tools/deviceTools.d.ts +3 -0
  129. package/build/tools/deviceTools.d.ts.map +1 -0
  130. package/build/tools/deviceTools.js +296 -0
  131. package/build/tools/deviceTools.js.map +1 -0
  132. package/build/tools/executionTools.d.ts +3 -0
  133. package/build/tools/executionTools.d.ts.map +1 -0
  134. package/build/tools/executionTools.js +173 -0
  135. package/build/tools/executionTools.js.map +1 -0
  136. package/build/tools/interactionTools.d.ts +3 -0
  137. package/build/tools/interactionTools.d.ts.map +1 -0
  138. package/build/tools/interactionTools.js +414 -0
  139. package/build/tools/interactionTools.js.map +1 -0
  140. package/build/tools/logTools.d.ts +3 -0
  141. package/build/tools/logTools.d.ts.map +1 -0
  142. package/build/tools/logTools.js +371 -0
  143. package/build/tools/logTools.js.map +1 -0
  144. package/build/tools/metaTools.d.ts +7 -0
  145. package/build/tools/metaTools.d.ts.map +1 -0
  146. package/build/tools/metaTools.js +205 -0
  147. package/build/tools/metaTools.js.map +1 -0
  148. package/build/tools/networkTools.d.ts +3 -0
  149. package/build/tools/networkTools.d.ts.map +1 -0
  150. package/build/tools/networkTools.js +478 -0
  151. package/build/tools/networkTools.js.map +1 -0
  152. package/build/tools/reduxTools.d.ts +3 -0
  153. package/build/tools/reduxTools.d.ts.map +1 -0
  154. package/build/tools/reduxTools.js +83 -0
  155. package/build/tools/reduxTools.js.map +1 -0
  156. package/build/tools/screenshotTools.d.ts +3 -0
  157. package/build/tools/screenshotTools.d.ts.map +1 -0
  158. package/build/tools/screenshotTools.js +606 -0
  159. package/build/tools/screenshotTools.js.map +1 -0
  160. package/package.json +1 -1
@@ -1,4104 +1,11 @@
1
- import WebSocket from "ws";
2
- import { pendingExecutions, getNextMessageId, connectedApps } from "./state.js";
3
- import { getFirstConnectedApp, getConnectedAppByDevice, connectToDevice, clearReconnectionSuppression, purgeStaleConnectionsForPorts } from "./connection.js";
4
- import { fetchDevices, selectMainDevice, filterDebuggableDevices, scanMetroPorts } from "./metro.js";
5
- import { DEFAULT_RECONNECTION_CONFIG, cancelReconnectionTimer } from "./connectionState.js";
6
- // Hermes runtime compatibility: polyfill for 'global' which doesn't exist in Hermes
7
- // In Hermes, globalThis is the standard way to access global scope
8
- const GLOBAL_POLYFILL = `var global = typeof global !== 'undefined' ? global : globalThis;`;
9
- /**
10
- * Check if a string contains emoji or other problematic Unicode characters
11
- * Hermes has issues with certain UTF-16 surrogate pairs (like emoji)
12
- */
13
- export function containsProblematicUnicode(str) {
14
- // Detect UTF-16 surrogate pairs (emoji and other characters outside BMP)
15
- // These cause "Invalid UTF-8 code point" errors in Hermes
16
- // eslint-disable-next-line no-control-regex
17
- return /[\uD800-\uDFFF]/.test(str);
18
- }
19
- /**
20
- * Strip leading comments from an expression
21
- * Users often start with // comments which break the (return expr) wrapping
22
- */
23
- export function stripLeadingComments(expression) {
24
- let result = expression;
25
- // Strip leading whitespace first
26
- result = result.trimStart();
27
- // Repeatedly strip leading single-line comments (// ...)
28
- while (result.startsWith("//")) {
29
- const newlineIndex = result.indexOf("\n");
30
- if (newlineIndex === -1) {
31
- // Entire expression is a comment
32
- return "";
33
- }
34
- result = result.slice(newlineIndex + 1).trimStart();
35
- }
36
- // Strip leading multi-line comments (/* ... */)
37
- while (result.startsWith("/*")) {
38
- const endIndex = result.indexOf("*/");
39
- if (endIndex === -1) {
40
- // Unclosed comment
41
- return result;
42
- }
43
- result = result.slice(endIndex + 2).trimStart();
44
- }
45
- return result;
46
- }
47
- /**
48
- * Validate and preprocess an expression before execution
49
- * Returns cleaned expression or error with helpful message
50
- */
51
- export function validateAndPreprocessExpression(expression) {
52
- // Check for emoji/problematic Unicode before any processing
53
- if (containsProblematicUnicode(expression)) {
54
- return {
55
- valid: false,
56
- expression,
57
- error: "Expression contains emoji or special Unicode characters that Hermes cannot compile. " +
58
- "Please remove emoji and use ASCII characters only."
59
- };
60
- }
61
- // Strip leading comments that would break the expression wrapper
62
- const cleaned = stripLeadingComments(expression);
63
- if (!cleaned.trim()) {
64
- return {
65
- valid: false,
66
- expression,
67
- error: "Expression is empty or contains only comments."
68
- };
69
- }
70
- // Check for top-level async that Hermes doesn't support in Runtime.evaluate
71
- // Pattern: starts with (async or async keyword at expression level
72
- const trimmed = cleaned.trim();
73
- if (trimmed.startsWith("(async") || trimmed.startsWith("async ") || trimmed.startsWith("async(")) {
74
- return {
75
- valid: false,
76
- expression: cleaned,
77
- error: "Hermes does not support top-level async functions in Runtime.evaluate. " +
78
- "Instead of `(async () => { ... })()`, use a synchronous approach or " +
79
- "execute the async code and access the result via a global variable: " +
80
- "`global.__result = null; myAsyncFn().then(r => global.__result = r)`"
81
- };
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
- }
93
- // Check for multi-statement expressions. Runtime.evaluate compiles input as
94
- // a single expression — `console.log('x'); 1+1` raises `')' expected at end
95
- // of parenthesized expression`. Internal callers wrap in (function(){...})()
96
- // so any `;` they use is at brace depth 1 and won't be flagged.
97
- if (hasTopLevelStatementSeparator(trimmed)) {
98
- return {
99
- valid: false,
100
- expression: cleaned,
101
- error: "Multi-statement expressions are not supported by Hermes Runtime.evaluate " +
102
- "(compiles input as a single expression). " +
103
- "Wrap the body in an IIFE: `(function(){ stmt1; stmt2; return result; })()`."
104
- };
105
- }
106
- return {
107
- valid: true,
108
- expression: cleaned
109
- };
110
- }
111
- // Walk `src` tracking string/template/comment and bracket depth. Returns true
112
- // iff a `;` appears at depth 0 with non-whitespace following it (i.e. it
113
- // separates two top-level statements rather than terminating a single one).
114
- function hasTopLevelStatementSeparator(src) {
115
- let i = 0;
116
- let parens = 0;
117
- let braces = 0;
118
- let brackets = 0;
119
- while (i < src.length) {
120
- const ch = src[i];
121
- const next = src[i + 1];
122
- if (ch === "/" && next === "/") {
123
- const nl = src.indexOf("\n", i + 2);
124
- if (nl === -1)
125
- return false;
126
- i = nl + 1;
127
- continue;
128
- }
129
- if (ch === "/" && next === "*") {
130
- const end = src.indexOf("*/", i + 2);
131
- if (end === -1)
132
- return false;
133
- i = end + 2;
134
- continue;
135
- }
136
- if (ch === '"' || ch === "'" || ch === "`") {
137
- const quote = ch;
138
- i++;
139
- while (i < src.length) {
140
- if (src[i] === "\\") {
141
- i += 2;
142
- continue;
143
- }
144
- if (src[i] === quote) {
145
- i++;
146
- break;
147
- }
148
- i++;
149
- }
150
- continue;
151
- }
152
- if (ch === "(")
153
- parens++;
154
- else if (ch === ")")
155
- parens--;
156
- else if (ch === "{")
157
- braces++;
158
- else if (ch === "}")
159
- braces--;
160
- else if (ch === "[")
161
- brackets++;
162
- else if (ch === "]")
163
- brackets--;
164
- else if (ch === ";" && parens === 0 && braces === 0 && brackets === 0) {
165
- const rest = src.slice(i + 1).trim();
166
- if (rest.length > 0)
167
- return true;
168
- }
169
- i++;
170
- }
171
- return false;
172
- }
173
- // Error patterns that indicate a stale/destroyed context
174
- const CONTEXT_ERROR_PATTERNS = [
175
- "cannot find context",
176
- "execution context was destroyed",
177
- "target closed",
178
- "inspected target navigated",
179
- "session closed",
180
- "context with specified id",
181
- "no execution context",
182
- "runningdetached"
183
- ];
184
- /**
185
- * Check if an error indicates a stale page context
186
- */
187
- function isContextError(error) {
188
- if (!error)
189
- return false;
190
- const lowerError = error.toLowerCase();
191
- return CONTEXT_ERROR_PATTERNS.some((pattern) => lowerError.includes(pattern));
192
- }
193
- /**
194
- * Simple delay helper
195
- */
196
- function delay(ms) {
197
- return new Promise((resolve) => setTimeout(resolve, ms));
198
- }
199
- /**
200
- * Attempt quick reconnection to Metro
201
- */
202
- async function attemptQuickReconnect(preferredPort) {
203
- try {
204
- const ports = await scanMetroPorts();
205
- const targetPort = preferredPort && ports.includes(preferredPort) ? preferredPort : ports[0];
206
- if (!targetPort)
207
- return false;
208
- const devices = await fetchDevices(targetPort);
209
- const mainDevice = selectMainDevice(devices);
210
- if (!mainDevice)
211
- return false;
212
- await connectToDevice(mainDevice, targetPort);
213
- return true;
214
- }
215
- catch {
216
- return false;
217
- }
218
- }
219
- /**
220
- * Execute expression on a connected app (core implementation without retry)
221
- */
222
- async function executeExpressionCore(expression, awaitPromise, timeoutMs = 10000, targetApp) {
223
- const app = targetApp ?? getFirstConnectedApp();
224
- if (!app) {
225
- return { success: false, error: "No apps connected. Run 'scan_metro' first." };
226
- }
227
- if (app.ws.readyState !== WebSocket.OPEN) {
228
- return { success: false, error: "WebSocket connection is not open." };
229
- }
230
- // Validate and preprocess the expression
231
- const validation = validateAndPreprocessExpression(expression);
232
- if (!validation.valid) {
233
- return { success: false, error: validation.error };
234
- }
235
- const cleanedExpression = validation.expression;
236
- // Hermes CDP does not support awaitPromise — it serializes the Promise's
237
- // internal fields (_A, _x, _y, _z) instead of waiting for resolution.
238
- // When the caller wants awaitPromise, we handle it ourselves: wrap the
239
- // expression to store the resolved value in a temp global, then poll.
240
- if (awaitPromise) {
241
- return executeWithManualAwait(app, cleanedExpression, timeoutMs);
242
- }
243
- return executeCDP(app, cleanedExpression, false, timeoutMs);
244
- }
245
- /**
246
- * Execute a CDP Runtime.evaluate call (no promise awaiting).
247
- */
248
- function executeCDP(app, cleanedExpression, awaitPromise, timeoutMs) {
249
- const TIMEOUT_MS = timeoutMs;
250
- const currentMessageId = getNextMessageId();
251
- const wrappedExpression = `${GLOBAL_POLYFILL} ${cleanedExpression}`;
252
- return new Promise((resolve) => {
253
- const timeoutId = setTimeout(() => {
254
- pendingExecutions.delete(currentMessageId);
255
- const wsState = app.ws.readyState === WebSocket.OPEN ? "OPEN"
256
- : app.ws.readyState === WebSocket.CLOSED ? "CLOSED"
257
- : app.ws.readyState === WebSocket.CLOSING ? "CLOSING"
258
- : "CONNECTING";
259
- const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
260
- const pageId = app.deviceInfo.id || "unknown";
261
- const truncatedExpr = cleanedExpression.length > 100
262
- ? cleanedExpression.substring(0, 100) + "..."
263
- : cleanedExpression;
264
- const errorMessage = [
265
- "Timeout: Expression took too long to evaluate.",
266
- "",
267
- `Connection state: ws=${wsState}, device="${deviceName}", platform=${app.platform}, pageId=${pageId}`,
268
- `Expression (truncated): ${truncatedExpr}`,
269
- "",
270
- "This usually means the JavaScript execution context became unresponsive or the CDP page is stale.",
271
- "",
272
- "Recovery steps (try in order):",
273
- "1. Call scan_metro to re-establish a fresh CDP connection",
274
- "2. If scan_metro doesn't help, force-restart the app:",
275
- " - iOS: ios_terminate_app then ios_launch_app",
276
- " - Android: android_launch_app (restarts automatically)",
277
- "3. After restarting, call scan_metro again to reconnect",
278
- ].join("\n");
279
- resolve({ success: false, error: errorMessage });
280
- }, TIMEOUT_MS);
281
- pendingExecutions.set(currentMessageId, { resolve, timeoutId });
282
- try {
283
- app.ws.send(JSON.stringify({
284
- id: currentMessageId,
285
- method: "Runtime.evaluate",
286
- params: {
287
- expression: wrappedExpression,
288
- returnByValue: true,
289
- awaitPromise,
290
- userGesture: true,
291
- generatePreview: true
292
- }
293
- }));
294
- }
295
- catch (error) {
296
- clearTimeout(timeoutId);
297
- pendingExecutions.delete(currentMessageId);
298
- resolve({
299
- success: false,
300
- error: `Failed to send: ${error instanceof Error ? error.message : String(error)}`
301
- });
302
- }
303
- });
304
- }
305
- /**
306
- * Hermes workaround for awaitPromise: execute the expression, and if it
307
- * returns a Promise, store the resolved/rejected value in a temp global
308
- * and read it back with a small number of spaced-out retries.
309
- */
310
- async function executeWithManualAwait(app, cleanedExpression, timeoutMs) {
311
- const slotId = `__rn_dbg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
312
- // Wrap: run the expression, if result is thenable store resolved value in
313
- // a temp global slot; otherwise store the sync value immediately.
314
- const wrapperExpr = `(function(){
315
- var __v=(${cleanedExpression});
316
- if(__v&&typeof __v==='object'&&typeof __v.then==='function'){
317
- globalThis['${slotId}']={s:'pending'};
318
- __v.then(function(r){globalThis['${slotId}']={s:'ok',v:r}},function(e){globalThis['${slotId}']={s:'err',v:String(e)}});
319
- return '__awaiting__'}
320
- else{return __v}})()`;
321
- const initial = await executeCDP(app, wrapperExpr, false, timeoutMs);
322
- // If the expression didn't return a Promise, return the result directly
323
- if (!initial.success || initial.result !== "__awaiting__") {
324
- return initial;
325
- }
326
- // Read the settled value with a few spaced-out retries (not aggressive polling).
327
- // Most Promises resolve within a microtask or a single event loop tick.
328
- const RETRY_DELAYS_MS = [100, 300, 600, 1000, 2000, 3000];
329
- const readExpr = `(function(){var s=globalThis['${slotId}'];if(!s||s.s==='pending')return '__pending__';delete globalThis['${slotId}'];return{status:s.s,value:s.v}})()`;
330
- for (const delayMs of RETRY_DELAYS_MS) {
331
- await new Promise((r) => setTimeout(r, delayMs));
332
- const poll = await executeCDP(app, readExpr, false, 5000);
333
- if (!poll.success)
334
- return poll;
335
- if (poll.result === "__pending__")
336
- continue;
337
- // The poll result comes through formatRemoteObject — objects are
338
- // JSON.stringified, so we need to parse it back.
339
- try {
340
- const parsed = typeof poll.result === "string" ? JSON.parse(poll.result) : poll.result;
341
- if (parsed?.status === "err") {
342
- return { success: false, error: parsed.value || "Promise rejected" };
343
- }
344
- const value = parsed?.value;
345
- return {
346
- success: true,
347
- result: value === undefined || value === null
348
- ? String(value)
349
- : typeof value === "object"
350
- ? JSON.stringify(value, null, 2)
351
- : String(value)
352
- };
353
- }
354
- catch {
355
- return poll;
356
- }
357
- }
358
- // Cleanup on timeout
359
- await executeCDP(app, `delete globalThis['${slotId}']`, false, 2000).catch(() => { });
360
- return { success: false, error: "Timeout: Promise did not resolve within the time limit." };
361
- }
362
- // Execute JavaScript in the connected React Native app with retry logic
363
- export async function executeInApp(expression, awaitPromise = true, options = {}, device) {
364
- const { maxRetries = 2, retryDelayMs = 1000, autoReconnect = true, timeoutMs = 10000 } = options;
365
- let lastError;
366
- let preferredPort;
367
- // Get preferred port from current connection if available
368
- const currentApp = getConnectedAppByDevice(device);
369
- if (currentApp) {
370
- preferredPort = currentApp.port;
371
- }
372
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
373
- const app = getConnectedAppByDevice(device);
374
- // No connection - try to reconnect if enabled
375
- if (!app) {
376
- if (autoReconnect && attempt < maxRetries) {
377
- console.error(`[execbro] No connection, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
378
- const reconnected = await attemptQuickReconnect(preferredPort);
379
- if (reconnected) {
380
- await delay(retryDelayMs);
381
- continue;
382
- }
383
- }
384
- return { success: false, error: "No apps connected. Run 'scan_metro' first." };
385
- }
386
- // WebSocket not open - try to reconnect
387
- if (app.ws.readyState !== WebSocket.OPEN) {
388
- if (autoReconnect && attempt < maxRetries) {
389
- console.error(`[execbro] WebSocket not open, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
390
- // Close stale connection
391
- const appKey = `${app.port}-${app.deviceInfo.id}`;
392
- cancelReconnectionTimer(appKey);
393
- try {
394
- app.ws.close();
395
- }
396
- catch {
397
- /* ignore */
398
- }
399
- connectedApps.delete(appKey);
400
- const reconnected = await attemptQuickReconnect(app.port);
401
- if (reconnected) {
402
- await delay(retryDelayMs);
403
- continue;
404
- }
405
- }
406
- return { success: false, error: "WebSocket connection is not open." };
407
- }
408
- // Execute the expression
409
- const result = await executeExpressionCore(expression, awaitPromise, timeoutMs, app);
410
- // Success - return result
411
- if (result.success) {
412
- return result;
413
- }
414
- lastError = result.error;
415
- // Check if this is a context error that might be recoverable
416
- if (isContextError(result.error)) {
417
- if (autoReconnect && attempt < maxRetries) {
418
- console.error(`[execbro] Context error detected, attempting reconnect (attempt ${attempt + 1}/${maxRetries})...`);
419
- // Close and reconnect
420
- const appKey = `${app.port}-${app.deviceInfo.id}`;
421
- cancelReconnectionTimer(appKey);
422
- try {
423
- app.ws.close();
424
- }
425
- catch {
426
- /* ignore */
427
- }
428
- connectedApps.delete(appKey);
429
- const reconnected = await attemptQuickReconnect(app.port);
430
- if (reconnected) {
431
- await delay(retryDelayMs);
432
- continue;
433
- }
434
- }
435
- }
436
- // Non-context error or no more retries - return error
437
- return result;
438
- }
439
- return {
440
- success: false,
441
- error: lastError ?? [
442
- "Execution failed after all retries. Connection may be stale.",
443
- "",
444
- "Recovery steps (try in order):",
445
- "1. Call scan_metro to re-establish a fresh CDP connection",
446
- "2. If scan_metro doesn't help, force-restart the app:",
447
- " - iOS: ios_terminate_app then ios_launch_app",
448
- " - Android: android_launch_app (restarts automatically)",
449
- "3. After restarting, call scan_metro again to reconnect",
450
- ].join("\n")
451
- };
452
- }
453
- // List globally available debugging objects in the app.
454
- // Uses Object.getOwnPropertyNames + Object.keys to catch non-enumerable globals
455
- // (Hermes does not always make `globalThis.x = ...` assignments enumerable),
456
- // and probes __RN_AI_DEVTOOLS__ directly so the SDK's registered stores/
457
- // navigation/custom objects surface as ready-to-use dotted paths.
458
- export async function listDebugGlobals(device) {
459
- const expression = `
460
- (function() {
461
- var names;
462
- try {
463
- var enumNames = Object.keys(globalThis);
464
- var ownNames = Object.getOwnPropertyNames(globalThis);
465
- var seen = {};
466
- names = [];
467
- for (var i = 0; i < ownNames.length; i++) { if (!seen[ownNames[i]]) { seen[ownNames[i]] = 1; names.push(ownNames[i]); } }
468
- for (var j = 0; j < enumNames.length; j++) { if (!seen[enumNames[j]]) { seen[enumNames[j]] = 1; names.push(enumNames[j]); } }
469
- } catch (e) {
470
- names = Object.keys(globalThis);
471
- }
472
-
473
- var categories = {
474
- 'Apollo Client': [],
475
- 'Redux': [],
476
- 'React DevTools': [],
477
- 'Reanimated': [],
478
- 'Expo': [],
479
- 'Metro': [],
480
- 'Other Debug': []
481
- };
482
- for (var k = 0; k < names.length; k++) {
483
- var key = names[k];
484
- if (key.indexOf('APOLLO') >= 0) categories['Apollo Client'].push(key);
485
- else if (key.indexOf('REDUX') >= 0) categories['Redux'].push(key);
486
- else if (key.indexOf('REACT_DEVTOOLS') >= 0) categories['React DevTools'].push(key);
487
- else if (key.indexOf('reanimated') >= 0 || key.indexOf('worklet') >= 0) categories['Reanimated'].push(key);
488
- else if (key.indexOf('Expo') >= 0 || key.indexOf('expo') >= 0) categories['Expo'].push(key);
489
- else if (key.indexOf('METRO') >= 0) categories['Metro'].push(key);
490
- else if (key.indexOf('__') === 0) categories['Other Debug'].push(key);
491
- }
492
-
493
- // SDK probe: detect __RN_AI_DEVTOOLS__ even when not enumerable on
494
- // globalThis, and flatten its registered objects into dotted paths
495
- // the agent can hand straight to inspect_global / execute_in_app.
496
- var sdk = null;
497
- try {
498
- if (typeof globalThis.__RN_AI_DEVTOOLS__ !== 'undefined' && globalThis.__RN_AI_DEVTOOLS__) {
499
- var dt = globalThis.__RN_AI_DEVTOOLS__;
500
- var paths = [];
501
- // Map well-known store keys back into the legacy category
502
- // buckets so SDK-registered stores don't appear missing to
503
- // agents that scan categories.Redux / categories["Apollo Client"].
504
- var storeCategory = {
505
- redux: 'Redux',
506
- apollo: 'Apollo Client',
507
- apolloclient: 'Apollo Client',
508
- reactdevtools: 'React DevTools'
509
- };
510
- if (dt.stores && typeof dt.stores === 'object') {
511
- var sk = Object.keys(dt.stores);
512
- for (var a = 0; a < sk.length; a++) {
513
- var storeKey = sk[a];
514
- var path = '__RN_AI_DEVTOOLS__.stores.' + storeKey;
515
- paths.push(path);
516
- var bucket = storeCategory[storeKey.toLowerCase()];
517
- if (bucket && categories[bucket].indexOf(path) < 0) {
518
- categories[bucket].push(path);
519
- }
520
- }
521
- }
522
- if (dt.navigation) paths.push('__RN_AI_DEVTOOLS__.navigation');
523
- if (dt.custom && typeof dt.custom === 'object') {
524
- var ck = Object.keys(dt.custom);
525
- for (var b = 0; b < ck.length; b++) paths.push('__RN_AI_DEVTOOLS__.custom.' + ck[b]);
526
- }
527
- sdk = {
528
- version: dt.version || 'unknown',
529
- capabilities: dt.capabilities || null,
530
- paths: paths,
531
- hint: 'These paths are inspect_global / execute_in_app ready (dotted paths supported).'
532
- };
533
- // Make sure the root global also appears in the listing,
534
- // even if Hermes hid it from Object.keys.
535
- if (categories['Other Debug'].indexOf('__RN_AI_DEVTOOLS__') < 0) {
536
- categories['Other Debug'].push('__RN_AI_DEVTOOLS__');
537
- }
538
- }
539
- } catch (e) { /* ignore */ }
540
-
541
- return { sdk: sdk, categories: categories };
542
- })()
543
- `;
544
- return executeInApp(expression, false, {}, device);
545
- }
546
- // Inspect a global object (or a dotted path into one) to see its properties
547
- // and types. Accepts plain identifiers (`__APOLLO_CLIENT__`) and dotted paths
548
- // (`__RN_AI_DEVTOOLS__.stores.redux`) so the discovery output from
549
- // listDebugGlobals can be passed straight back in.
550
- export async function inspectGlobal(objectName, device) {
551
- // Reject anything that isn't a safe dotted identifier path. This both
552
- // prevents accidental code execution via objectName and produces a clear
553
- // error instead of a confusing Hermes parse failure.
554
- if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(objectName)) {
555
- return {
556
- success: false,
557
- error: `Invalid objectName: '${objectName}'. Expected an identifier or dotted path like '__APOLLO_CLIENT__' or '__RN_AI_DEVTOOLS__.stores.redux'. For arbitrary expressions, use execute_in_app.`
558
- };
559
- }
560
- const expression = `
561
- (function() {
562
- var obj;
563
- try { obj = ${objectName}; } catch (e) { return { error: 'NotFound: ' + (e && e.message ? e.message : String(e)) }; }
564
- if (obj === undefined) return { error: 'Object not found' };
565
- if (obj === null) return { error: 'Value is null' };
566
- var t = typeof obj;
567
- if (t !== 'object' && t !== 'function') {
568
- return { __value: obj, __type: t };
569
- }
570
- var result = {};
571
- var keys;
572
- try { keys = Object.keys(obj); } catch (e) { keys = []; }
573
- for (var i = 0; i < keys.length; i++) {
574
- var key = keys[i];
575
- var val;
576
- try { val = obj[key]; } catch (e) { result[key] = { type: 'unknown', callable: false, error: 'getter threw' }; continue; }
577
- var type = typeof val;
578
- if (type === 'function') {
579
- result[key] = { type: 'function', callable: true };
580
- } else if (type === 'object' && val !== null) {
581
- var preview;
582
- try { preview = JSON.stringify(val); } catch (e) { preview = '[unserializable]'; }
583
- if (preview && preview.length > 100) preview = preview.slice(0, 100) + '...';
584
- result[key] = { type: Array.isArray(val) ? 'array' : 'object', callable: false, preview: preview };
585
- } else {
586
- result[key] = { type: type, callable: false, value: val };
587
- }
588
- }
589
- return result;
590
- })()
591
- `;
592
- return executeInApp(expression, false, {}, device);
593
- }
594
- // Reload the React Native app using __ReactRefresh
595
- // Note: Page.reload CDP method may work on Bridgeless targets (via HostAgent) — not yet tested
596
- // Uses fire-and-forget: sends the reload command without waiting for a response,
597
- // since the JS context is destroyed during reload and would always timeout.
598
- export async function reloadApp(device) {
599
- // Get current connection info before reload
600
- let app = getConnectedAppByDevice(device);
601
- // Auto-connect if no connection exists. Mirrors scan_metro's flow
602
- // (clearReconnectionSuppression + filterDebuggableDevices + purge +
603
- // connectToDevice per device) — earlier attempts that used ensureConnection
604
- // here raced with WS close events on first connect and reported
605
- // "Connection succeeded but app is not available". scan_metro's pattern
606
- // is the empirically-stable path.
607
- if (!app) {
608
- console.error("[execbro] No connection for reload, attempting auto-connect...");
609
- const ports = await scanMetroPorts();
610
- if (ports.length === 0) {
611
- return {
612
- success: false,
613
- error: "No apps connected and no Metro server found. Make sure Metro bundler is running (npm start or expo start), then try again."
614
- };
615
- }
616
- clearReconnectionSuppression();
617
- const portDevices = new Map();
618
- for (const port of ports) {
619
- const devices = await fetchDevices(port);
620
- const debuggable = filterDebuggableDevices(devices);
621
- if (debuggable.length > 0) {
622
- portDevices.set(port, debuggable);
623
- }
624
- }
625
- purgeStaleConnectionsForPorts(portDevices);
626
- for (const [port, devices] of portDevices) {
627
- for (const dev of devices) {
628
- try {
629
- await connectToDevice(dev, port);
630
- }
631
- catch (error) {
632
- console.error(`[execbro] Auto-connect failed for ${dev.title} on port ${port}: ${error}`);
633
- }
634
- }
635
- }
636
- app = getConnectedAppByDevice(device);
637
- if (!app) {
638
- return {
639
- success: false,
640
- error: "No apps connected. Found Metro server but could not connect to any device. Make sure the React Native app is running."
641
- };
642
- }
643
- }
644
- const port = app.port;
645
- // Fire-and-forget: send reload command via CDP without waiting for response.
646
- // The JS context is destroyed during reload, so Runtime.evaluate would always timeout.
647
- const reloadExpression = `(function() {
648
- try {
649
- if (typeof __ReactRefresh !== 'undefined' && typeof __ReactRefresh.performFullRefresh === 'function') {
650
- __ReactRefresh.performFullRefresh('mcp-reload');
651
- return 'ok';
652
- }
653
- if (typeof global !== 'undefined' && global.DevSettings && typeof global.DevSettings.reload === 'function') {
654
- global.DevSettings.reload();
655
- return 'ok';
656
- }
657
- return 'no-method';
658
- } catch (e) { return 'error:' + e.message; }
659
- })()`;
660
- try {
661
- if (app.ws.readyState !== WebSocket.OPEN) {
662
- const deviceName = app.deviceInfo.deviceName || app.deviceInfo.title || "unknown";
663
- return {
664
- success: false,
665
- error: [
666
- `WebSocket connection is not open (device="${deviceName}", platform=${app.platform}).`,
667
- "The CDP page may be stale or the app has crashed.",
668
- "",
669
- "Recovery steps (try in order):",
670
- "1. Call scan_metro to re-establish a fresh CDP connection",
671
- "2. If scan_metro doesn't help, force-restart the app:",
672
- " - iOS: ios_terminate_app then ios_launch_app",
673
- " - Android: android_launch_app (restarts automatically)",
674
- "3. After restarting, call scan_metro again to reconnect",
675
- ].join("\n")
676
- };
677
- }
678
- // Send without registering a pending execution — fire and forget
679
- const messageId = getNextMessageId();
680
- app.ws.send(JSON.stringify({
681
- id: messageId,
682
- method: "Runtime.evaluate",
683
- params: {
684
- expression: reloadExpression,
685
- returnByValue: true,
686
- awaitPromise: false,
687
- userGesture: true
688
- }
689
- }));
690
- }
691
- catch (error) {
692
- return {
693
- success: false,
694
- error: `Failed to send reload command: ${error instanceof Error ? error.message : String(error)}`
695
- };
696
- }
697
- // Auto-reconnect after reload
698
- try {
699
- // Wait for app to reload (give it time to restart JS context)
700
- await delay(2000);
701
- // Find and close only the targeted device's connection (not all devices on this port)
702
- const targetDeviceId = app.deviceInfo.id;
703
- for (const [key, connectedApp] of connectedApps.entries()) {
704
- if (connectedApp.deviceInfo.id === targetDeviceId) {
705
- cancelReconnectionTimer(key);
706
- try {
707
- connectedApp.ws.close();
708
- }
709
- catch {
710
- // Ignore close errors
711
- }
712
- connectedApps.delete(key);
713
- break;
714
- }
715
- }
716
- // Small delay to ensure cleanup
717
- await delay(500);
718
- // Reconnect only the reloaded device (not all devices on the port)
719
- const devices = await fetchDevices(port);
720
- const targetDevice = devices.find(d => d.id === targetDeviceId)
721
- || devices.find(d => d.deviceName === app.deviceInfo.deviceName);
722
- if (targetDevice) {
723
- await connectToDevice(targetDevice, port, {
724
- isReconnection: false,
725
- reconnectionConfig: { ...DEFAULT_RECONNECTION_CONFIG, enabled: false }
726
- });
727
- return {
728
- success: true,
729
- result: `App reloaded and reconnected to ${targetDevice.deviceName || targetDevice.title}`
730
- };
731
- }
732
- else {
733
- return {
734
- success: true,
735
- result: "App reloaded but could not auto-reconnect. Run 'scan_metro' to reconnect."
736
- };
737
- }
738
- }
739
- catch (error) {
740
- return {
741
- success: true,
742
- result: `App reloaded but auto-reconnect failed: ${error instanceof Error ? error.message : String(error)}. Run 'scan_metro' to reconnect.`
743
- };
744
- }
745
- }
746
- function formatTreeToTonl(node, indent = 0) {
747
- const prefix = " ".repeat(indent);
748
- let result = `${prefix}${node.component}`;
749
- // Add props inline if present
750
- if (node.props && Object.keys(node.props).length > 0) {
751
- const propsStr = Object.entries(node.props)
752
- .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
753
- .join(",");
754
- result += ` (${propsStr})`;
755
- }
756
- // Add layout inline if present
757
- if (node.layout && Object.keys(node.layout).length > 0) {
758
- const layoutStr = Object.entries(node.layout)
759
- .map(([k, v]) => `${k}:${v}`)
760
- .join(",");
761
- result += ` [${layoutStr}]`;
762
- }
763
- result += "\n";
764
- // Recurse children
765
- if (node.children && node.children.length > 0) {
766
- for (const child of node.children) {
767
- result += formatTreeToTonl(child, indent + 1);
768
- }
769
- }
770
- return result;
771
- }
772
- // Ultra-compact structure-only tree format (just component names, indented)
773
- function formatTreeStructureOnly(node, indent = 0) {
774
- const prefix = " ".repeat(indent);
775
- let result = `${prefix}${node.component}\n`;
776
- if (node.children && node.children.length > 0) {
777
- for (const child of node.children) {
778
- result += formatTreeStructureOnly(child, indent + 1);
779
- }
780
- }
781
- return result;
782
- }
783
- /**
784
- * Classify each root as "screen" or "overlay".
785
- *
786
- * A root is a **screen** if its subtree contains a navigation screen marker
787
- * (Route(...), *Screen, *Page). Falls back to the largest root by area
788
- * if no navigation markers are found. Everything else is an overlay.
789
- */
790
- function classifyRoots(roots, elements, childrenMap) {
791
- const labels = [];
792
- // Navigation screen markers: Route(...) wrapper or user-defined *Screen/*Page
793
- const screenMarker = /^(Route\(|.*Screen\(|.*Page\(|.*Screen$|.*Page$)/;
794
- function subtreeHasScreen(idx, depth) {
795
- if (depth > 30)
796
- return false;
797
- if (screenMarker.test(elements[idx].component))
798
- return true;
799
- const kids = childrenMap.get(idx);
800
- if (kids) {
801
- for (const kid of kids) {
802
- if (subtreeHasScreen(kid, depth + 1))
803
- return true;
804
- }
805
- }
806
- return false;
807
- }
808
- // First pass: check which roots contain a navigation screen
809
- const hasScreen = roots.map(ri => subtreeHasScreen(ri, 0));
810
- const anyScreenFound = hasScreen.some(Boolean);
811
- if (anyScreenFound) {
812
- for (let i = 0; i < roots.length; i++) {
813
- labels.push(hasScreen[i] ? "screen" : "overlay");
814
- }
815
- }
816
- else {
817
- // Fallback: largest root by area is the screen
818
- let maxArea = 0;
819
- let maxIdx = 0;
820
- for (let i = 0; i < roots.length; i++) {
821
- const f = elements[roots[i]].frame;
822
- if (f && f.width * f.height > maxArea) {
823
- maxArea = f.width * f.height;
824
- maxIdx = i;
825
- }
826
- }
827
- for (let i = 0; i < roots.length; i++) {
828
- labels.push(i === maxIdx ? "screen" : "overlay");
829
- }
830
- }
831
- return { labels, hasOverlays: labels.some(l => l === "overlay") };
832
- }
833
- /**
834
- * Build, collapse, classify, and render a layout tree.
835
- *
836
- * Shared by both get_screen_layout (points, no tap coords) and
837
- * the screenshot layout enrichment (pixels, with tap coords).
838
- *
839
- * @param elements - flat list of layout nodes with parentIndex linkage
840
- * @param renderLine - callback that produces the output line for a node,
841
- * given (element, indent level, whether it's a leaf)
842
- */
843
- function formatLayoutTree(elements, renderLine) {
844
- // Build index: originalIndex -> element index in the filtered array
845
- const indexMap = new Map();
846
- for (let i = 0; i < elements.length; i++) {
847
- if (elements[i].originalIndex !== undefined) {
848
- indexMap.set(elements[i].originalIndex, i);
849
- }
850
- }
851
- // Build children lists
852
- const children = new Map();
853
- const roots = [];
854
- for (let i = 0; i < elements.length; i++) {
855
- const el = elements[i];
856
- const parentOrigIdx = el.parentIndex;
857
- if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
858
- roots.push(i);
859
- }
860
- else {
861
- const parentIdx = indexMap.get(parentOrigIdx);
862
- if (!children.has(parentIdx))
863
- children.set(parentIdx, []);
864
- children.get(parentIdx).push(i);
865
- }
866
- }
867
- // Collapse wrapper chains: if a child has the same frame as its parent,
868
- // it's just a wrapper — promote its children and text to the parent
869
- function sameFrame(a, b) {
870
- if (!a.frame || !b.frame)
871
- return false;
872
- return Math.abs(a.frame.x - b.frame.x) < 1 &&
873
- Math.abs(a.frame.y - b.frame.y) < 1 &&
874
- Math.abs(a.frame.width - b.frame.width) < 1 &&
875
- Math.abs(a.frame.height - b.frame.height) < 1;
876
- }
877
- function collapseNode(parentIdx) {
878
- const kids = children.get(parentIdx);
879
- if (!kids)
880
- return;
881
- const newKids = [];
882
- for (const kidIdx of kids) {
883
- if (sameFrame(elements[parentIdx], elements[kidIdx])) {
884
- if (!elements[parentIdx].text && elements[kidIdx].text) {
885
- elements[parentIdx].text = elements[kidIdx].text;
886
- }
887
- const grandKids = children.get(kidIdx);
888
- if (grandKids) {
889
- newKids.push(...grandKids);
890
- }
891
- }
892
- else {
893
- newKids.push(kidIdx);
894
- }
895
- }
896
- if (newKids.length > 0) {
897
- children.set(parentIdx, newKids);
898
- }
899
- else {
900
- children.delete(parentIdx);
901
- }
902
- const updatedKids = children.get(parentIdx);
903
- if (updatedKids) {
904
- for (const kid of updatedKids) {
905
- collapseNode(kid);
906
- }
907
- }
908
- }
909
- for (const root of roots) {
910
- collapseNode(root);
911
- }
912
- // Render tree
913
- const lines = [];
914
- function printNode(idx, indent) {
915
- const isLeaf = !children.has(idx);
916
- lines.push(renderLine(elements[idx], indent, isLeaf));
917
- const kids = children.get(idx);
918
- if (kids) {
919
- for (const kid of kids) {
920
- printNode(kid, indent + 1);
921
- }
922
- }
923
- }
924
- // Classify roots and emit layer headers
925
- const { labels, hasOverlays } = classifyRoots(roots, elements, children);
926
- if (hasOverlays) {
927
- let prevLabel = "";
928
- for (let ri = 0; ri < roots.length; ri++) {
929
- const label = labels[ri];
930
- if (label === "overlay" || label !== prevLabel) {
931
- if (lines.length > 0)
932
- lines.push("");
933
- lines.push(`[${label}]`);
934
- }
935
- prevLabel = label;
936
- printNode(roots[ri], 0);
937
- }
938
- }
939
- else {
940
- for (const root of roots) {
941
- printNode(root, 0);
942
- }
943
- }
944
- return lines.join("\n");
945
- }
946
- export function formatScreenLayoutTree(elements, extended = false, offScreen) {
947
- const tree = formatLayoutTree(elements, (el, indent, isLeaf) => {
948
- const prefix = " ".repeat(indent);
949
- const frame = el.frame
950
- ? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
951
- : "";
952
- const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
953
- const idStr = id ? ` [${id}]` : "";
954
- const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
955
- const layoutStr = extended && el.layout
956
- ? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
957
- : "";
958
- return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
959
- });
960
- const suffix = [];
961
- if (offScreen?.offScreenAbove?.length) {
962
- suffix.push(formatOffScreenLine(offScreen.offScreenAbove, "above fold"));
963
- }
964
- if (offScreen?.offScreenBelow?.length) {
965
- suffix.push(formatOffScreenLine(offScreen.offScreenBelow, "below fold"));
966
- }
967
- return suffix.length > 0 ? `${tree}\n\n${suffix.join("\n")}` : tree;
968
- }
969
- function formatOffScreenLine(names, position) {
970
- const total = names.length;
971
- const CAP = 10;
972
- if (total <= CAP) {
973
- return `[... ${total} component${total === 1 ? "" : "s"} ${position}: ${names.join(", ")}]`;
974
- }
975
- const shown = names.slice(0, CAP).join(", ");
976
- return `[... ${total} components ${position}: ${shown}, ... +${total - CAP} more]`;
977
- }
978
- function formatFoundComponentsToTonl(components) {
979
- const lines = ["#found{component,path,depth,key,layout}"];
980
- for (const c of components) {
981
- const layout = c.layout
982
- ? Object.entries(c.layout)
983
- .map(([k, v]) => `${k}:${v}`)
984
- .join(";")
985
- : "";
986
- lines.push(`${c.component}|${c.path}|${c.depth}|${c.key || ""}|${layout}`);
987
- }
988
- return lines.join("\n");
989
- }
990
- function formatSummaryToTonl(components, total) {
991
- const lines = [`#summary total=${total}`];
992
- for (const c of components) {
993
- lines.push(`${c.component}:${c.count}`);
994
- }
995
- return lines.join("\n");
996
- }
997
- /**
998
- * Get the React component tree from the running app.
999
- * This traverses the fiber tree to extract component hierarchy with names.
1000
- */
1001
- export async function getComponentTree(options = {}) {
1002
- const { includeProps = false, includeStyles = false, hideInternals = true, format = "tonl", structureOnly = false, focusedOnly = false, device } = options;
1003
- // Use lower default depth for structureOnly to keep output compact (~2-5KB)
1004
- // Full mode uses higher depth since TONL format handles it better
1005
- // focusedOnly mode uses moderate depth since we're already filtering to active screen
1006
- const maxDepth = options.maxDepth ?? 5000;
1007
- const expression = `
1008
- (function() {
1009
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1010
- if (!hook) return { error: 'React DevTools hook not found. Make sure you are running a development build.' };
1011
-
1012
- // Try to get fiber roots (renderer ID is usually 1)
1013
- let roots = [];
1014
- if (hook.getFiberRoots) {
1015
- roots = [...(hook.getFiberRoots(1) || [])];
1016
- }
1017
- if (roots.length === 0 && hook.renderers) {
1018
- // Try all renderers
1019
- for (const [id] of hook.renderers) {
1020
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
1021
- if (r.length > 0) {
1022
- roots = r;
1023
- break;
1024
- }
1025
- }
1026
- }
1027
- if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
1028
-
1029
- const maxDepth = ${maxDepth};
1030
- const includeProps = ${includeProps};
1031
- const includeStyles = ${includeStyles};
1032
- const hideInternals = ${hideInternals};
1033
- const focusedOnly = ${focusedOnly};
1034
-
1035
- // Internal RN components to hide
1036
- const internalPatterns = /^(RCT|RNS|Animated\\(|AnimatedComponent|VirtualizedList|CellRenderer|ScrollViewContext|PerformanceLoggerContext|RootTagContext|HeaderShownContext|HeaderHeightContext|HeaderBackContext|SafeAreaFrameContext|SafeAreaInsetsContext|VirtualizedListContext|VirtualizedListCellContextProvider|StaticContainer|DelayedFreeze|Freeze|Suspender|DebugContainer|MaybeNestedStack|SceneView|NavigationContent|PreventRemoveProvider|EnsureSingleNavigator)/;
1037
-
1038
- // Screen component patterns - user's actual screens (strict matching)
1039
- // Only match *Screen and *Page to avoid false positives like BottomTabView
1040
- const screenPatterns = /^[A-Z][a-zA-Z0-9]*(Screen|Page)$/;
1041
-
1042
- // Navigation/internal screen patterns to SKIP (these look like screens but are framework components)
1043
- const internalScreenPatterns = /^(MaybeScreen|Screen$|ScreenContainer|ScreenStack|SceneView|Background$)/;
1044
-
1045
- // Provider/wrapper patterns to skip when finding focused screen
1046
- const wrapperPatterns = /^(App|AppContainer|Provider|Context|SafeArea|Gesture|Theme|Redux|Root|Navigator|Stack|Tab|Drawer|Navigation|Container|Wrapper|Layout|ErrorBoundary|Suspense|PersistGate|LinkingContext|AppState|View|Fragment|NativeStack|BottomTab|Screen$)/i;
1047
-
1048
- // Global overlay patterns - stop traversing into these subtrees
1049
- // Be specific to avoid blocking BottomSheetDrawer, PortalProvider, etc.
1050
- const overlayPatterns = /^(BottomSheet$|BottomSheetGlobal|Modal$|Toast$|Snackbar$|Dialog$|Overlay$|Popup$|MyToast$|PaywallModal$|FullScreenBannerModal$)/i;
1051
-
1052
- // Navigation container patterns - skip traversing into these (screens inside are nav screens, not focused content)
1053
- const navContainerPatterns = /^(RootNavigation|NativeStackNavigator|BottomTabNavigator|DrawerNavigator|TabNavigator|StackNavigator)/;
1054
-
1055
- function getComponentName(fiber) {
1056
- if (!fiber || !fiber.type) return null;
1057
- if (typeof fiber.type === 'string') return fiber.type; // Host component (View, Text, etc.)
1058
- return fiber.type.displayName || fiber.type.name || null;
1059
- }
1060
-
1061
- function shouldHide(name) {
1062
- if (!hideInternals || !name) return false;
1063
- return internalPatterns.test(name);
1064
- }
1065
-
1066
- function extractLayoutStyles(style) {
1067
- try {
1068
- if (!style) return null;
1069
- const merged = Array.isArray(style)
1070
- ? Object.assign({}, ...style.filter(Boolean).map(s => {
1071
- try { return typeof s === 'object' ? s : {}; }
1072
- catch { return {}; }
1073
- }))
1074
- : (typeof style === 'object' ? style : {});
1075
-
1076
- const layout = {};
1077
- const layoutKeys = [
1078
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1079
- 'paddingHorizontal', 'paddingVertical',
1080
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
1081
- 'marginHorizontal', 'marginVertical',
1082
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
1083
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
1084
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
1085
- 'position', 'top', 'bottom', 'left', 'right',
1086
- 'gap', 'rowGap', 'columnGap',
1087
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'
1088
- ];
1089
-
1090
- for (const key of layoutKeys) {
1091
- if (merged[key] !== undefined) layout[key] = merged[key];
1092
- }
1093
- return Object.keys(layout).length > 0 ? layout : null;
1094
- } catch { return null; }
1095
- }
1096
-
1097
- function walkFiber(fiber, depth) {
1098
- if (!fiber || depth > maxDepth) return null;
1099
-
1100
- const name = getComponentName(fiber);
1101
-
1102
- // Skip anonymous/internal components unless they have meaningful children
1103
- if (!name || shouldHide(name)) {
1104
- // Still traverse children
1105
- let child = fiber.child;
1106
- const children = [];
1107
- while (child) {
1108
- const childResult = walkFiber(child, depth);
1109
- if (childResult) children.push(childResult);
1110
- child = child.sibling;
1111
- }
1112
- // Return first meaningful child or null
1113
- return children.length === 1 ? children[0] : (children.length > 1 ? { component: '(Fragment)', children } : null);
1114
- }
1115
-
1116
- const node = { component: name };
1117
-
1118
- // Include props if requested (excluding children and style for cleaner output)
1119
- if (includeProps && fiber.memoizedProps) {
1120
- const props = {};
1121
- for (const key of Object.keys(fiber.memoizedProps)) {
1122
- if (key === 'children' || key === 'style') continue;
1123
- try {
1124
- const val = fiber.memoizedProps[key];
1125
- if (typeof val === 'function') {
1126
- props[key] = '[Function]';
1127
- } else if (typeof val === 'object' && val !== null) {
1128
- props[key] = Array.isArray(val) ? '[Array]' : '[Object]';
1129
- } else {
1130
- props[key] = val;
1131
- }
1132
- } catch {
1133
- props[key] = '[Animated Value]';
1134
- }
1135
- }
1136
- if (Object.keys(props).length > 0) node.props = props;
1137
- }
1138
-
1139
- // Include layout styles if requested
1140
- try {
1141
- if (includeStyles && fiber.memoizedProps?.style) {
1142
- const layout = extractLayoutStyles(fiber.memoizedProps.style);
1143
- if (layout) node.layout = layout;
1144
- }
1145
- } catch { /* animated style — skip */ }
1146
-
1147
- // Traverse children
1148
- let child = fiber.child;
1149
- const children = [];
1150
- while (child) {
1151
- const childResult = walkFiber(child, depth + 1);
1152
- if (childResult) children.push(childResult);
1153
- child = child.sibling;
1154
- }
1155
- if (children.length > 0) node.children = children;
1156
-
1157
- return node;
1158
- }
1159
-
1160
- // Find focused screen if requested
1161
- function findFocusedScreen(fiber, depth = 0) {
1162
- if (!fiber || depth > 5000) return null;
1163
-
1164
- const name = getComponentName(fiber);
1165
-
1166
- // Skip overlays (BottomSheet, Modal, Toast, etc.) - don't traverse into them
1167
- if (name && overlayPatterns.test(name)) {
1168
- return null;
1169
- }
1170
-
1171
- // Skip navigation containers - screens inside are nav screens, not focused content
1172
- if (name && navContainerPatterns.test(name)) {
1173
- return null;
1174
- }
1175
-
1176
- // Check if this is a user's screen component (not framework internals)
1177
- if (name && screenPatterns.test(name) && !wrapperPatterns.test(name) && !internalScreenPatterns.test(name)) {
1178
- return fiber;
1179
- }
1180
-
1181
- // Search children
1182
- let child = fiber.child;
1183
- while (child) {
1184
- const found = findFocusedScreen(child, depth + 1);
1185
- if (found) return found;
1186
- child = child.sibling;
1187
- }
1188
-
1189
- return null;
1190
- }
1191
-
1192
- let startFiber = roots[0].current;
1193
- let focusedScreenName = null;
1194
-
1195
- if (focusedOnly) {
1196
- const focused = findFocusedScreen(roots[0].current);
1197
- if (focused) {
1198
- startFiber = focused;
1199
- focusedScreenName = getComponentName(focused);
1200
- }
1201
- }
1202
-
1203
- const tree = walkFiber(startFiber, 0);
1204
-
1205
- if (focusedOnly && focusedScreenName) {
1206
- return { focusedScreen: focusedScreenName, tree };
1207
- }
1208
- return { tree };
1209
- })()
1210
- `;
1211
- // Use a longer timeout for component tree traversal — large apps can exceed 10s
1212
- const result = await executeInApp(expression, false, { timeoutMs: 30000 }, device);
1213
- // Apply formatting if requested
1214
- if (result.success && result.result) {
1215
- try {
1216
- const parsed = JSON.parse(result.result);
1217
- if (parsed.tree) {
1218
- const prefix = parsed.focusedScreen ? `Focused: ${parsed.focusedScreen}\n\n` : "";
1219
- // Structure-only mode: ultra-compact format with just component names
1220
- if (structureOnly) {
1221
- const structure = formatTreeStructureOnly(parsed.tree);
1222
- return { success: true, result: prefix + structure };
1223
- }
1224
- // TONL format: compact with props/layout
1225
- if (format === "tonl") {
1226
- const tonl = formatTreeToTonl(parsed.tree);
1227
- return { success: true, result: prefix + tonl };
1228
- }
1229
- }
1230
- }
1231
- catch {
1232
- // If parsing fails, return original result
1233
- }
1234
- }
1235
- return result;
1236
- }
1237
- /**
1238
- * Get layout data for visible components on the current screen.
1239
- * Uses measureInWindow to get actual screen positions and filters
1240
- * to only components within the viewport.
1241
- *
1242
- * Two-step approach (same as inspectAtPoint):
1243
- * Step 1: Walk fiber tree, dispatch measureInWindow on host components
1244
- * Step 2: After 300ms, read measurements, filter by viewport, build results
1245
- */
1246
- export async function getScreenLayout(options = {}) {
1247
- const { extended = false, summary = false, device, raw = false } = options;
1248
- const maxDepth = 5000;
1249
- const componentsOnly = true;
1250
- const shortPath = true;
1251
- // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
1252
- const dispatchExpression = `
1253
- (function() {
1254
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1255
- if (!hook) return { error: 'React DevTools hook not found.' };
1256
-
1257
- var roots = [];
1258
- if (hook.getFiberRoots) {
1259
- roots = Array.from(hook.getFiberRoots(1) || []);
1260
- }
1261
- if (roots.length === 0 && hook.renderers) {
1262
- for (var entry of hook.renderers) {
1263
- var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
1264
- if (r.length > 0) { roots = r; break; }
1265
- }
1266
- }
1267
- if (roots.length === 0) return { error: 'No fiber roots found.' };
1268
-
1269
- function getMeasurable(fiber) {
1270
- var sn = fiber.stateNode;
1271
- if (!sn) return null;
1272
- if (typeof sn.measureInWindow === 'function') return sn;
1273
- if (sn.canonical && sn.canonical.publicInstance &&
1274
- typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1275
- return sn.canonical.publicInstance;
1276
- }
1277
- // Fabric leaf nodes like RCTText have no publicInstance. Measure via
1278
- // the native Fabric UIManager using the shadow node instead — needed
1279
- // for text bounds that are tight around the glyphs (not the scroll
1280
- // container the text happens to live inside).
1281
- if (sn.node && globalThis.nativeFabricUIManager &&
1282
- typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
1283
- var node = sn.node;
1284
- return {
1285
- measureInWindow: function(cb) {
1286
- try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
1287
- }
1288
- };
1289
- }
1290
- return null;
1291
- }
1292
-
1293
- // Collect host fibers with their metadata
1294
- var hostFibers = [];
1295
- var fiberMeta = [];
1296
- var componentsOnlyMode = ${componentsOnly};
1297
-
1298
- // RN internals and primitives — skip these when looking for meaningful component names
1299
- // Only filter internal components that can't be written as JSX.
1300
- // Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
1301
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|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)$/;
1302
-
1303
- function getComponentName(fiber) {
1304
- if (!fiber || !fiber.type) return null;
1305
- if (typeof fiber.type === 'string') return fiber.type;
1306
- return fiber.type.displayName || fiber.type.name || null;
1307
- }
1308
-
1309
- // Find the first measurable host descendant of a fiber
1310
- function findFirstHost(fiber, depth) {
1311
- if (!fiber || depth > 20) return null;
1312
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
1313
- var child = fiber.child;
1314
- while (child) {
1315
- var found = findFirstHost(child, depth + 1);
1316
- if (found) return found;
1317
- child = child.sibling;
1318
- }
1319
- return null;
1320
- }
1321
-
1322
- // Extract text content from a fiber subtree.
1323
- // When a fiber has a string child, return it without recursing
1324
- // (avoids duplication from Text > RCTText having the same string).
1325
- function collectText(fiber, d) {
1326
- if (!fiber || d > 30) return '';
1327
- var props = fiber.memoizedProps;
1328
- if (props) {
1329
- var ch = props.children;
1330
- // Leaf text — return without recursing into children fibers
1331
- if (typeof ch === 'string') return ch;
1332
- if (typeof ch === 'number') return String(ch);
1333
- if (Array.isArray(ch)) {
1334
- var inline = [];
1335
- for (var ci = 0; ci < ch.length; ci++) {
1336
- if (typeof ch[ci] === 'string') inline.push(ch[ci]);
1337
- else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
1338
- }
1339
- if (inline.length > 0) return inline.join('');
1340
- }
1341
- }
1342
- // No direct text — collect from child fibers (siblings = adjacent elements)
1343
- var parts = [];
1344
- var child = fiber.child;
1345
- while (child) {
1346
- var t = collectText(child, d + 1);
1347
- if (t) parts.push(t);
1348
- child = child.sibling;
1349
- }
1350
- return parts.join(' ').trim();
1351
- }
1352
-
1353
- if (componentsOnlyMode) {
1354
- // componentsOnly: walk tree looking for meaningful custom components,
1355
- // measure their first host child, track parent for tree output
1356
- function walkComponents(fiber, depth, path, parentIdx, ancestors) {
1357
- if (!fiber || depth > ${maxDepth}) return;
1358
- var name = getComponentName(fiber);
1359
- var isHost = typeof fiber.type === 'string';
1360
-
1361
- // Skip inactive screens (react-native-screens MaybeScreen with active=0)
1362
- // active: 0 = inactive/detached, 1 = transitioning, 2 = active
1363
- if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
1364
-
1365
- // Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
1366
- if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
1367
-
1368
- var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
1369
-
1370
- var myIdx = parentIdx;
1371
- var nextAncestors = ancestors;
1372
- if (isMeaningful) {
1373
- var host = findFirstHost(fiber, 0);
1374
- if (host) {
1375
- myIdx = hostFibers.length;
1376
- // Extract text from this component's subtree
1377
- var text = collectText(fiber, 0);
1378
- hostFibers.push(host);
1379
- fiberMeta.push({
1380
- hostName: typeof host.type === 'string' ? host.type : '',
1381
- customName: name,
1382
- depth: depth,
1383
- path: path.concat([name]),
1384
- parentIndex: parentIdx,
1385
- ancestorIndices: ancestors.slice(),
1386
- text: text ? text.slice(0, 80) : null
1387
- });
1388
- // Build the ancestor chain for descendants: [myIdx, ...previous ancestors]
1389
- nextAncestors = [myIdx].concat(ancestors);
1390
- }
1391
- }
1392
-
1393
- var child = fiber.child;
1394
- while (child) {
1395
- var childName = getComponentName(child);
1396
- walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx, nextAncestors);
1397
- child = child.sibling;
1398
- }
1399
- }
1400
- for (var ri = 0; ri < roots.length; ri++) {
1401
- walkComponents(roots[ri].current, 0, [], -1, []);
1402
- }
1403
- } else {
1404
- // Default mode: collect all host fibers with ancestor info
1405
- function walkFibers(fiber, depth, path) {
1406
- if (!fiber || depth > ${maxDepth}) return;
1407
- var name = getComponentName(fiber);
1408
- var isHost = typeof fiber.type === 'string';
1409
-
1410
- // Skip inactive screens (react-native-screens MaybeScreen with active=0)
1411
- if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
1412
-
1413
- // Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
1414
- if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
1415
-
1416
- if (name && isHost && getMeasurable(fiber)) {
1417
- // Find nearest meaningful custom component ancestor for display
1418
- var customName = null;
1419
- var fallbackName = null;
1420
- var cur = fiber.return;
1421
- while (cur) {
1422
- if (cur.type && typeof cur.type !== 'string') {
1423
- var cName = cur.type.displayName || cur.type.name || null;
1424
- if (cName) {
1425
- if (!fallbackName) fallbackName = cName;
1426
- if (!RN_PRIMITIVES.test(cName)) {
1427
- customName = cName;
1428
- break;
1429
- }
1430
- }
1431
- }
1432
- cur = cur.return;
1433
- }
1434
- if (!customName) customName = fallbackName;
1435
-
1436
- hostFibers.push(fiber);
1437
- fiberMeta.push({
1438
- hostName: name,
1439
- customName: customName,
1440
- depth: depth,
1441
- path: path.slice()
1442
- });
1443
- }
1444
-
1445
- var child = fiber.child;
1446
- while (child) {
1447
- var childName = getComponentName(child);
1448
- walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
1449
- child = child.sibling;
1450
- }
1451
- }
1452
- for (var ri = 0; ri < roots.length; ri++) {
1453
- walkFibers(roots[ri].current, 0, []);
1454
- }
1455
- }
1456
-
1457
- if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
1458
-
1459
- // Store fibers and metadata globally for step 2
1460
- globalThis.__layoutFibers = hostFibers;
1461
- globalThis.__layoutMeta = fiberMeta;
1462
- globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
1463
-
1464
- // Dispatch measureInWindow on all host fibers
1465
- for (var i = 0; i < hostFibers.length; i++) {
1466
- try {
1467
- (function(idx) {
1468
- getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
1469
- globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
1470
- });
1471
- })(i);
1472
- } catch(e) {}
1473
- }
1474
-
1475
- return { count: hostFibers.length };
1476
- })()
1477
- `;
1478
- let dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
1479
- if (!dispatchResult.success)
1480
- return dispatchResult;
1481
- let dispatchError;
1482
- try {
1483
- const parsed = JSON.parse(dispatchResult.result || "{}");
1484
- if (parsed.error)
1485
- dispatchError = parsed.error;
1486
- }
1487
- catch {
1488
- /* ignore */
1489
- }
1490
- // Retry once on the early-startup race where the React DevTools hook
1491
- // isn't registered yet. Production / non-__DEV__ builds will still fail
1492
- // after the retry — surface an actionable error pointing at OCR/screenshot.
1493
- if (dispatchError && /React DevTools hook not found/.test(dispatchError)) {
1494
- await delay(400);
1495
- dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
1496
- if (!dispatchResult.success)
1497
- return dispatchResult;
1498
- dispatchError = undefined;
1499
- try {
1500
- const parsed = JSON.parse(dispatchResult.result || "{}");
1501
- if (parsed.error)
1502
- dispatchError = parsed.error;
1503
- }
1504
- catch {
1505
- /* ignore */
1506
- }
1507
- if (dispatchError && /React DevTools hook not found/.test(dispatchError)) {
1508
- return {
1509
- success: false,
1510
- error: "React DevTools hook not registered (likely a production / non-__DEV__ build). " +
1511
- "Fiber-based layout is unavailable. Use ocr_screenshot for text + tap coordinates, " +
1512
- "or ios_screenshot / android_screenshot for a visual snapshot.",
1513
- };
1514
- }
1515
- }
1516
- if (dispatchError)
1517
- return { success: false, error: dispatchError };
1518
- // Wait for measureInWindow callbacks
1519
- await delay(300);
1520
- // --- Step 2: read measurements, filter visible, build results ---
1521
- const resolveExpression = `
1522
- (function() {
1523
- var fibers = globalThis.__layoutFibers;
1524
- var meta = globalThis.__layoutMeta;
1525
- var measurements = globalThis.__layoutMeasurements;
1526
- globalThis.__layoutFibers = null;
1527
- globalThis.__layoutMeta = null;
1528
- globalThis.__layoutMeasurements = null;
1529
-
1530
- if (!fibers || !measurements || !meta) {
1531
- return { error: 'No measurement data. Run get_screen_layout again.' };
1532
- }
1533
-
1534
- var componentsOnly = ${componentsOnly};
1535
- var shortPath = ${shortPath};
1536
- var summaryMode = ${summary};
1537
- var pathSegments = 3;
1538
-
1539
-
1540
- // Get viewport dimensions from the first root view measurement
1541
- // Accept elements starting at x=0 even with negative y (safe area extensions)
1542
- var viewportW = 9999, viewportH = 9999;
1543
- for (var v = 0; v < measurements.length; v++) {
1544
- if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
1545
- measurements[v].width > 0 && measurements[v].height > 0) {
1546
- viewportW = measurements[v].width;
1547
- // For wrappers extending behind safe area, the visible viewport height
1548
- // is the total height minus the negative offset
1549
- viewportH = measurements[v].height + measurements[v].y;
1550
- break;
1551
- }
1552
- }
1553
-
1554
- function formatPath(pathArray) {
1555
- if (!shortPath || pathArray.length <= pathSegments) {
1556
- return pathArray.join(' > ');
1557
- }
1558
- return '... > ' + pathArray.slice(-pathSegments).join(' > ');
1559
- }
1560
-
1561
- function extractLayoutStyles(style) {
1562
- try {
1563
- if (!style) return null;
1564
- var merged = Array.isArray(style)
1565
- ? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) {
1566
- try { return typeof s === 'object' ? s : {}; }
1567
- catch(e) { return {}; }
1568
- })))
1569
- : (typeof style === 'object' ? style : {});
1570
-
1571
- var layout = {};
1572
- var layoutKeys = [
1573
- 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
1574
- 'paddingHorizontal', 'paddingVertical',
1575
- 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
1576
- 'marginHorizontal', 'marginVertical',
1577
- 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
1578
- 'flex', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink',
1579
- 'justifyContent', 'alignItems', 'alignSelf', 'alignContent',
1580
- 'position', 'top', 'bottom', 'left', 'right',
1581
- 'gap', 'rowGap', 'columnGap',
1582
- 'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
1583
- 'backgroundColor', 'borderColor', 'borderRadius',
1584
- 'zIndex', 'elevation'
1585
- ];
1586
-
1587
- for (var k = 0; k < layoutKeys.length; k++) {
1588
- if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
1589
- }
1590
- return Object.keys(layout).length > 0 ? layout : null;
1591
- } catch(e) { return null; }
1592
- }
1593
-
1594
- var elements = [];
1595
- var offScreenBelow = [];
1596
- var offScreenAbove = [];
1597
-
1598
- for (var i = 0; i < measurements.length; i++) {
1599
- var m = measurements[i];
1600
- if (!m) continue;
1601
-
1602
- // Filter: zero-size
1603
- if (m.width <= 0 || m.height <= 0) continue;
1604
-
1605
- // Vertical: track off-screen above/below for the summary line
1606
- if (m.y + m.height < 0) {
1607
- if (meta[i] && meta[i].customName) offScreenAbove.push({ idx: i, y: m.y });
1608
- continue;
1609
- }
1610
- if (m.y > viewportH) {
1611
- if (meta[i] && meta[i].customName) offScreenBelow.push({ idx: i, y: m.y });
1612
- continue;
1613
- }
1614
-
1615
- // Horizontal: center must be inside the viewport (drops phantom slides at x=-411 etc.)
1616
- var centerX = m.x + m.width / 2;
1617
- if (centerX < 0 || centerX > viewportW) continue;
1618
-
1619
- var fiber = fibers[i];
1620
- var info = meta[i];
1621
-
1622
- var displayName = componentsOnly ? info.customName : info.hostName;
1623
- if (!displayName) continue;
1624
- if (componentsOnly && !info.customName) continue;
1625
-
1626
- var style = null;
1627
- try { style = fiber.memoizedProps ? fiber.memoizedProps.style : null; } catch {}
1628
- var layout = extractLayoutStyles(style);
1629
-
1630
- // Get text content — use pre-collected text from step 1, or fall back to host fiber
1631
- var textContent = info.text || null;
1632
- if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
1633
- var children = null;
1634
- try { children = fiber.memoizedProps ? fiber.memoizedProps.children : null; } catch {}
1635
- if (typeof children === 'string') textContent = children;
1636
- else if (typeof children === 'number') textContent = String(children);
1637
- }
1638
-
1639
- var element = {
1640
- component: displayName,
1641
- path: formatPath(info.path),
1642
- depth: info.depth,
1643
- frame: { x: m.x, y: m.y, width: m.width, height: m.height },
1644
- originalIndex: i
1645
- };
1646
-
1647
- if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
1648
- if (layout) element.layout = layout;
1649
- if (textContent) element.text = textContent.slice(0, 100);
1650
-
1651
- // Identifiers
1652
- if (fiber.memoizedProps) {
1653
- var identifiers = {};
1654
- if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
1655
- if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
1656
- if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
1657
- if (fiber.key) identifiers.key = fiber.key;
1658
- if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
1659
- }
1660
-
1661
- elements.push(element);
1662
- }
1663
-
1664
- // In componentsOnly mode, remove full-screen wrapper components
1665
- // and re-parent their children to collapse the wrapper chain
1666
- if (componentsOnly && elements.length > 0) {
1667
- var filtered = [];
1668
- // Map: originalIndex -> resolved parent for skipped wrappers
1669
- var reparent = {};
1670
-
1671
- for (var fi = 0; fi < elements.length; fi++) {
1672
- var el = elements[fi];
1673
- var fr = el.frame;
1674
- // A wrapper is full-screen if it covers the entire viewport or extends beyond it
1675
- // (e.g., y=-119 wrappers that extend behind the safe area)
1676
- var isFullScreen = fr && fr.x <= 0 && fr.y <= 0 &&
1677
- (fr.width >= viewportW - 2) && (fr.y + fr.height >= viewportH - 2);
1678
-
1679
- if (isFullScreen) {
1680
- // Skip this wrapper, map its originalIndex to its parent
1681
- reparent[el.originalIndex] = el.parentIndex;
1682
- } else {
1683
- // Resolve parent through any skipped wrappers
1684
- var resolvedParent = el.parentIndex;
1685
- while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
1686
- resolvedParent = reparent[resolvedParent];
1687
- }
1688
- el.parentIndex = resolvedParent;
1689
- filtered.push(el);
1690
- }
1691
- }
1692
- elements = filtered;
1693
-
1694
- // Second pass: rewrite orphans whose parentIndex points at a non-surviving element.
1695
- // This happens when an intermediate ancestor was dropped by the viewport filter
1696
- // (not the full-screen wrapper path). Walk ancestorIndices to the nearest survivor.
1697
- var surviving = {};
1698
- for (var si = 0; si < elements.length; si++) {
1699
- surviving[elements[si].originalIndex] = true;
1700
- }
1701
- for (var oi = 0; oi < elements.length; oi++) {
1702
- var e2 = elements[oi];
1703
- if (e2.parentIndex === -1 || e2.parentIndex === undefined) continue;
1704
- if (surviving[e2.parentIndex]) continue;
1705
- var anc = meta[e2.originalIndex] && meta[e2.originalIndex].ancestorIndices;
1706
- if (!anc) { e2.parentIndex = -1; continue; }
1707
- var found = -1;
1708
- for (var ai = 0; ai < anc.length; ai++) {
1709
- if (surviving[anc[ai]]) { found = anc[ai]; break; }
1710
- }
1711
- e2.parentIndex = found;
1712
- }
1713
- }
1714
-
1715
- if (summaryMode) {
1716
- var counts = {};
1717
- for (var j = 0; j < elements.length; j++) {
1718
- counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
1719
- }
1720
- var sorted = Object.keys(counts).map(function(name) {
1721
- return { component: name, count: counts[name] };
1722
- }).sort(function(a, b) { return b.count - a.count; });
1723
- return {
1724
- totalElements: elements.length,
1725
- uniqueComponents: sorted.length,
1726
- components: sorted
1727
- };
1728
- }
1729
-
1730
- return {
1731
- viewport: { width: viewportW, height: viewportH },
1732
- totalElements: elements.length,
1733
- elements: elements,
1734
- offScreenBelow: (function() {
1735
- var seen = {}, out = [];
1736
- offScreenBelow.sort(function(a, b) { return a.y - b.y; });
1737
- for (var k = 0; k < offScreenBelow.length; k++) {
1738
- var n = meta[offScreenBelow[k].idx].customName;
1739
- if (n && !seen[n]) { seen[n] = true; out.push(n); }
1740
- }
1741
- return out;
1742
- })(),
1743
- offScreenAbove: (function() {
1744
- var seen = {}, out = [];
1745
- offScreenAbove.sort(function(a, b) { return b.y - a.y; });
1746
- for (var k = 0; k < offScreenAbove.length; k++) {
1747
- var n = meta[offScreenAbove[k].idx].customName;
1748
- if (n && !seen[n]) { seen[n] = true; out.push(n); }
1749
- }
1750
- return out;
1751
- })()
1752
- };
1753
- })()
1754
- `;
1755
- const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
1756
- // Format output as tree
1757
- if (result.success && result.result) {
1758
- try {
1759
- const parsed = JSON.parse(result.result);
1760
- if (parsed.components) {
1761
- // Summary mode
1762
- const tonl = formatSummaryToTonl(parsed.components, parsed.totalElements);
1763
- return { success: true, result: tonl };
1764
- }
1765
- else if (parsed.elements) {
1766
- if (raw) {
1767
- return {
1768
- success: true,
1769
- result: result.result,
1770
- parsedElements: parsed.elements,
1771
- viewport: parsed.viewport,
1772
- offScreenBelow: Array.isArray(parsed.offScreenBelow) ? parsed.offScreenBelow : [],
1773
- offScreenAbove: Array.isArray(parsed.offScreenAbove) ? parsed.offScreenAbove : []
1774
- };
1775
- }
1776
- const tree = formatScreenLayoutTree(parsed.elements, extended, {
1777
- offScreenBelow: Array.isArray(parsed.offScreenBelow) ? parsed.offScreenBelow : [],
1778
- offScreenAbove: Array.isArray(parsed.offScreenAbove) ? parsed.offScreenAbove : []
1779
- });
1780
- return { success: true, result: tree };
1781
- }
1782
- }
1783
- catch {
1784
- // If parsing fails, return original result
1785
- }
1786
- }
1787
- return result;
1788
- }
1789
- export async function getPressableElements(options = {}) {
1790
- const { device } = options;
1791
- // --- Step 1: walk fiber tree, find pressable/input elements, dispatch measureInWindow ---
1792
- const dispatchExpression = `
1793
- (function() {
1794
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1795
- if (!hook) return { error: 'React DevTools hook not found.' };
1796
-
1797
- var roots = [];
1798
- if (hook.getFiberRoots) {
1799
- roots = Array.from(hook.getFiberRoots(1) || []);
1800
- }
1801
- if (roots.length === 0 && hook.renderers) {
1802
- for (var entry of hook.renderers) {
1803
- var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
1804
- if (r.length > 0) { roots = r; break; }
1805
- }
1806
- }
1807
- if (roots.length === 0) return { error: 'No fiber roots found.' };
1808
-
1809
- function getMeasurable(fiber) {
1810
- var sn = fiber.stateNode;
1811
- if (!sn) return null;
1812
- if (typeof sn.measureInWindow === 'function') return sn;
1813
- if (sn.canonical && sn.canonical.publicInstance &&
1814
- typeof sn.canonical.publicInstance.measureInWindow === 'function') {
1815
- return sn.canonical.publicInstance;
1816
- }
1817
- // Fabric leaf nodes like RCTText have no publicInstance. Measure via
1818
- // the native Fabric UIManager using the shadow node instead — needed
1819
- // for text bounds that are tight around the glyphs (not the scroll
1820
- // container the text happens to live inside).
1821
- if (sn.node && globalThis.nativeFabricUIManager &&
1822
- typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
1823
- var node = sn.node;
1824
- return {
1825
- measureInWindow: function(cb) {
1826
- try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
1827
- }
1828
- };
1829
- }
1830
- return null;
1831
- }
1832
-
1833
- function findFirstHost(fiber, depth) {
1834
- if (!fiber || depth > 20) return null;
1835
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
1836
- var child = fiber.child;
1837
- while (child) {
1838
- var found = findFirstHost(child, depth + 1);
1839
- if (found) return found;
1840
- child = child.sibling;
1841
- }
1842
- return null;
1843
- }
1844
-
1845
- function findHostsInSubtree(fiber, depth, hosts, limit) {
1846
- if (!fiber || depth > 20 || hosts.length >= limit) return;
1847
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
1848
- hosts.push(fiber);
1849
- return;
1850
- }
1851
- var child = fiber.child;
1852
- while (child && hosts.length < limit) {
1853
- findHostsInSubtree(child, depth + 1, hosts, limit);
1854
- child = child.sibling;
1855
- }
1856
- }
1857
-
1858
- function collectText(fiber, d, isRoot) {
1859
- if (!fiber || d > 30) return '';
1860
- var props = fiber.memoizedProps;
1861
- // Stop descent at nested pressable/input boundaries — their text belongs to them, not to the outer wrapper.
1862
- if (!isRoot && props && (typeof props.onPress === 'function' ||
1863
- typeof props.onChangeText === 'function' ||
1864
- typeof props.onFocus === 'function')) {
1865
- return '';
1866
- }
1867
- if (props) {
1868
- var ch = props.children;
1869
- if (typeof ch === 'string') return ch;
1870
- if (typeof ch === 'number') return String(ch);
1871
- if (Array.isArray(ch)) {
1872
- var inline = [];
1873
- for (var ci = 0; ci < ch.length; ci++) {
1874
- if (typeof ch[ci] === 'string') inline.push(ch[ci]);
1875
- else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
1876
- }
1877
- if (inline.length > 0) return inline.join('');
1878
- }
1879
- }
1880
- var parts = [];
1881
- var child = fiber.child;
1882
- while (child) {
1883
- var t = collectText(child, d + 1, false);
1884
- if (t) parts.push(t);
1885
- child = child.sibling;
1886
- }
1887
- return parts.join(' ').trim();
1888
- }
1889
-
1890
- function getComponentName(fiber) {
1891
- if (!fiber || !fiber.type) return null;
1892
- if (typeof fiber.type === 'string') return fiber.type;
1893
- return fiber.type.displayName || fiber.type.name || null;
1894
- }
1895
-
1896
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|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)$/;
1897
-
1898
- var hostFibers = [];
1899
- var fiberMeta = [];
1900
- var textFibers = [];
1901
- var textContents = [];
1902
-
1903
- function findMeaningfulAncestorName(fiber) {
1904
- var cur = fiber.return;
1905
- var depth = 0;
1906
- var fallbackName = null;
1907
- while (cur && depth < 20) {
1908
- var name = getComponentName(cur);
1909
- if (name && typeof cur.type !== 'string') {
1910
- if (!fallbackName) fallbackName = name;
1911
- if (!RN_PRIMITIVES.test(name)) return name;
1912
- }
1913
- cur = cur.return;
1914
- depth++;
1915
- }
1916
- return fallbackName;
1917
- }
1918
-
1919
- // Layout-only and touch-wrapper components that should be skipped when scanning
1920
- // children for a meaningful icon/content component name.
1921
- var SKIP_IN_CHILD_SCAN = /^(View|Text|Image|ImageBackground|ScrollView|FlatList|SectionList|KeyboardAvoidingView|SafeAreaView|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|TouchableNativeFeedback|Pressable|TextInput|ActivityIndicator|Switch|Modal|StatusBar|VirtualizedList|RefreshControl|Animated\\(.*|withAnimated.*|AnimatedComponent.*)$/;
1922
-
1923
- // Scan children of a pressable for a meaningful component name (e.g. SvgChevronBack inside a View).
1924
- // Skips generic layout/touch wrappers to find the actual icon or content component.
1925
- function findMeaningfulChildName(fiber) {
1926
- function scan(f, d) {
1927
- if (!f || d > 12) return null;
1928
- var n = getComponentName(f);
1929
- if (n && typeof f.type !== 'string' && !RN_PRIMITIVES.test(n) && !SKIP_IN_CHILD_SCAN.test(n)) return n;
1930
- var c = f.child;
1931
- while (c) {
1932
- var r = scan(c, d + 1);
1933
- if (r) return r;
1934
- c = c.sibling;
1935
- }
1936
- return null;
1937
- }
1938
- return scan(fiber.child, 0);
1939
- }
1940
-
1941
- var GENERIC_COMPONENT = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText|Unknown)$/;
1942
-
1943
- function buildPath(fiber) {
1944
- var parts = [];
1945
- var cur = fiber;
1946
- var depth = 0;
1947
- while (cur && depth < 30) {
1948
- var name = getComponentName(cur);
1949
- if (name && typeof cur.type !== 'string' && !RN_PRIMITIVES.test(name)) {
1950
- parts.unshift(name);
1951
- }
1952
- cur = cur.return;
1953
- depth++;
1954
- }
1955
- // Keep last 3 segments
1956
- if (parts.length > 3) {
1957
- parts = parts.slice(-3);
1958
- return '... > ' + parts.join(' > ');
1959
- }
1960
- return parts.join(' > ');
1961
- }
1962
-
1963
- function walkPressables(fiber, depth) {
1964
- if (!fiber || depth > 5000) return;
1965
- var name = getComponentName(fiber);
1966
- var props = fiber.memoizedProps;
1967
-
1968
- // Skip inactive/unfocused screens
1969
- if (name === 'MaybeScreen' && props && props.active === 0) return;
1970
- if (name === 'SceneView' && props && props.focused === false) return;
1971
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) return;
1972
-
1973
- var isPressable = props && typeof props.onPress === 'function';
1974
- var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
1975
-
1976
- if (isPressable || isInput) {
1977
- var hostsForThis = [];
1978
- findHostsInSubtree(fiber, 0, hostsForThis, 16);
1979
- if (hostsForThis.length > 0) {
1980
- var text = collectText(fiber, 0, true);
1981
- var componentName = findMeaningfulAncestorName(fiber) || name || 'Unknown';
1982
- // If name is generic (e.g. View, TouchableOpacity), scan children for a
1983
- // meaningful name like SvgChevronBack so icon-only buttons are identifiable.
1984
- if (GENERIC_COMPONENT.test(componentName)) {
1985
- var childName = findMeaningfulChildName(fiber);
1986
- if (childName) componentName = childName;
1987
- }
1988
- var path = buildPath(fiber);
1989
- var testID = (props && (props.testID || props.nativeID)) || null;
1990
- var accessibilityLabel = (props && props.accessibilityLabel) || null;
1991
-
1992
- var hostIndices = [];
1993
- for (var hi = 0; hi < hostsForThis.length; hi++) {
1994
- hostIndices.push(hostFibers.length);
1995
- hostFibers.push(hostsForThis[hi]);
1996
- }
1997
- fiberMeta.push({
1998
- component: componentName,
1999
- path: path,
2000
- text: text ? text.slice(0, 100) : '',
2001
- testID: testID,
2002
- accessibilityLabel: accessibilityLabel,
2003
- isInput: !!isInput,
2004
- hostIndices: hostIndices
2005
- });
2006
- }
2007
- }
2008
-
2009
- var child = fiber.child;
2010
- while (child) {
2011
- walkPressables(child, depth + 1);
2012
- child = child.sibling;
2013
- }
2014
- }
2015
-
2016
- walkPressables(roots[0].current, 0);
2017
-
2018
- // Collect text fibers that are NOT inside a pressable — used later to attach
2019
- // spatial 'nearbyText' hints to icon-only pressables.
2020
- function extractTextString(fiber) {
2021
- var p = fiber.memoizedProps;
2022
- if (!p) return '';
2023
- var ch = p.children;
2024
- if (typeof ch === 'string') return ch;
2025
- if (typeof ch === 'number') return String(ch);
2026
- if (Array.isArray(ch)) {
2027
- var parts = [];
2028
- for (var k = 0; k < ch.length; k++) {
2029
- if (typeof ch[k] === 'string') parts.push(ch[k]);
2030
- else if (typeof ch[k] === 'number') parts.push(String(ch[k]));
2031
- }
2032
- if (parts.length > 0) return parts.join('');
2033
- }
2034
- return '';
2035
- }
2036
-
2037
- function walkTexts(fiber, depth, insidePressable, inHidden) {
2038
- if (!fiber || depth > 5000) return;
2039
- var name = getComponentName(fiber);
2040
- var props = fiber.memoizedProps;
2041
-
2042
- // Track hidden-screen ancestry without early return — a MaybeScreen wrapper
2043
- // can enclose BOTH the active and inactive siblings, so we must keep walking
2044
- // to find the active one while still skipping the inactive subtree's texts.
2045
- var nextHidden = inHidden;
2046
- if (name === 'MaybeScreen' && props && props.active === 0) nextHidden = true;
2047
- if (name === 'SceneView' && props && props.focused === false) nextHidden = true;
2048
- if (name === 'RNSScreen' && props && props['aria-hidden'] === true) nextHidden = true;
2049
-
2050
- var hasOnPress = props && typeof props.onPress === 'function';
2051
- var isInputHere = props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
2052
- var nextInside = insidePressable || !!hasOnPress || !!isInputHere;
2053
-
2054
- // Record standalone text when outside any pressable. Detects both stock
2055
- // Text and custom wrappers (CustomText, ThemedText, etc.) by checking for
2056
- // direct string/number children. Fabric RCTText has no publicInstance, so
2057
- // we walk up the fiber chain to the nearest measurable host (the View
2058
- // enclosing the text) and use its frame as the text's proxy bounds.
2059
- if (!insidePressable && !nextHidden && name !== 'RCTText' && typeof fiber.type !== 'string') {
2060
- var str = extractTextString(fiber);
2061
- if (str && str.length > 0 && str.length <= 120) {
2062
- var up = fiber;
2063
- var upDepth = 0;
2064
- var measurable = null;
2065
- while (up && upDepth < 20) {
2066
- if (typeof up.type === 'string' && getMeasurable(up)) {
2067
- measurable = up;
2068
- break;
2069
- }
2070
- up = up.return;
2071
- upDepth++;
2072
- }
2073
- if (measurable) {
2074
- textFibers.push(measurable);
2075
- textContents.push(str);
2076
- }
2077
- }
2078
- }
2079
-
2080
- var child = fiber.child;
2081
- while (child) {
2082
- walkTexts(child, depth + 1, nextInside, nextHidden);
2083
- child = child.sibling;
2084
- }
2085
- }
2086
- walkTexts(roots[0].current, 0, false, false);
2087
-
2088
- if (hostFibers.length === 0) return { error: 'No pressable elements found on screen.' };
2089
-
2090
- // Also measure the root view for viewport detection (appended; tracked by explicit index to preserve hostIndices)
2091
- var rootHost = findFirstHost(roots[0].current, 0);
2092
- var rootIdx = -1;
2093
- if (rootHost) {
2094
- rootIdx = hostFibers.length;
2095
- hostFibers.push(rootHost);
2096
- }
2097
-
2098
- globalThis.__pressableFibers = hostFibers;
2099
- globalThis.__pressableMeta = fiberMeta;
2100
- globalThis.__pressableMeasurements = new Array(hostFibers.length).fill(null);
2101
- globalThis.__pressableRootIdx = rootIdx;
2102
-
2103
- for (var i = 0; i < hostFibers.length; i++) {
2104
- try {
2105
- (function(idx) {
2106
- getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
2107
- globalThis.__pressableMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
2108
- });
2109
- })(i);
2110
- } catch(e) {}
2111
- }
2112
-
2113
- globalThis.__pressableTextContents = textContents;
2114
- globalThis.__pressableTextMeasurements = new Array(textFibers.length).fill(null);
2115
- for (var ti = 0; ti < textFibers.length; ti++) {
2116
- try {
2117
- (function(idx) {
2118
- getMeasurable(textFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
2119
- globalThis.__pressableTextMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
2120
- });
2121
- })(ti);
2122
- } catch(e) {}
2123
- }
2124
-
2125
- return { count: hostFibers.length, textCount: textFibers.length };
2126
- })()
2127
- `;
2128
- const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
2129
- if (!dispatchResult.success)
2130
- return dispatchResult;
2131
- try {
2132
- const parsed = JSON.parse(dispatchResult.result || "{}");
2133
- if (parsed.error)
2134
- return { success: false, error: parsed.error };
2135
- }
2136
- catch {
2137
- /* ignore */
2138
- }
2139
- // Wait for measureInWindow callbacks
2140
- await delay(300);
2141
- // --- Step 2: read measurements, filter visible, build results ---
2142
- const resolveExpression = `
2143
- (function() {
2144
- var fibers = globalThis.__pressableFibers;
2145
- var meta = globalThis.__pressableMeta;
2146
- var measurements = globalThis.__pressableMeasurements;
2147
- var rootIdx = globalThis.__pressableRootIdx;
2148
- var textContents = globalThis.__pressableTextContents || [];
2149
- var textMeasurements = globalThis.__pressableTextMeasurements || [];
2150
- globalThis.__pressableFibers = null;
2151
- globalThis.__pressableMeta = null;
2152
- globalThis.__pressableMeasurements = null;
2153
- globalThis.__pressableRootIdx = null;
2154
- globalThis.__pressableTextContents = null;
2155
- globalThis.__pressableTextMeasurements = null;
2156
-
2157
- if (!fibers || !measurements || !meta) {
2158
- return { error: 'No measurement data. Run get_pressable_elements again.' };
2159
- }
2160
-
2161
- // Get viewport dimensions from the explicit root measurement (fallback to scanning)
2162
- var viewportW = 9999, viewportH = 9999;
2163
- var rootM = (rootIdx != null && rootIdx >= 0) ? measurements[rootIdx] : null;
2164
- if (rootM && rootM.width > 0 && rootM.height > 0) {
2165
- viewportW = rootM.width;
2166
- viewportH = rootM.height + (rootM.y > 0 ? rootM.y : 0);
2167
- } else {
2168
- for (var v = 0; v < measurements.length; v++) {
2169
- if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
2170
- measurements[v].width > 0 && measurements[v].height > 0) {
2171
- viewportW = measurements[v].width;
2172
- viewportH = measurements[v].height + measurements[v].y;
2173
- break;
2174
- }
2175
- }
2176
- }
2177
-
2178
- var elements = [];
2179
-
2180
- for (var i = 0; i < meta.length; i++) {
2181
- var info = meta[i];
2182
-
2183
- // Union all host measurements for this pressable to get its true bounds
2184
- var uMinX = Infinity, uMinY = Infinity, uMaxX = -Infinity, uMaxY = -Infinity;
2185
- var hasValid = false;
2186
- var indices = info.hostIndices || [];
2187
- for (var hi2 = 0; hi2 < indices.length; hi2++) {
2188
- var mm = measurements[indices[hi2]];
2189
- if (!mm || mm.width <= 0 || mm.height <= 0) continue;
2190
- hasValid = true;
2191
- if (mm.x < uMinX) uMinX = mm.x;
2192
- if (mm.y < uMinY) uMinY = mm.y;
2193
- if (mm.x + mm.width > uMaxX) uMaxX = mm.x + mm.width;
2194
- if (mm.y + mm.height > uMaxY) uMaxY = mm.y + mm.height;
2195
- }
2196
- if (!hasValid) continue;
2197
- var m = { x: uMinX, y: uMinY, width: uMaxX - uMinX, height: uMaxY - uMinY };
2198
-
2199
- // Filter: only visible within viewport
2200
- if (m.width <= 0 || m.height <= 0) continue;
2201
- if (m.x + m.width < 0 || m.y + m.height < 0) continue;
2202
- if (m.x > viewportW || m.y > viewportH) continue;
2203
-
2204
- var text = info.text || '';
2205
-
2206
- elements.push({
2207
- component: info.component,
2208
- path: info.path,
2209
- center: {
2210
- x: Math.round(m.x + m.width / 2),
2211
- y: Math.round(m.y + m.height / 2)
2212
- },
2213
- frame: {
2214
- x: Math.round(m.x),
2215
- y: Math.round(m.y),
2216
- width: Math.round(m.width),
2217
- height: Math.round(m.height)
2218
- },
2219
- text: text,
2220
- testID: info.testID,
2221
- accessibilityLabel: info.accessibilityLabel,
2222
- hasLabel: text.length > 0,
2223
- isInput: info.isInput
2224
- });
2225
- }
2226
-
2227
- // Deduplicate: multiple nested pressables (View > TouchableOpacity > TouchableOpacity)
2228
- // often share the same frame. Keep the one with the most meaningful component name,
2229
- // but merge text/testID/accessibilityLabel from the loser so stopping collectText
2230
- // at nested-pressable boundaries does not drop labels across the merge.
2231
- var HOST_NAMES = /^(View|TouchableOpacity|TouchableHighlight|TouchableWithoutFeedback|Pressable|TouchableNativeFeedback|Text|RCTView|RCTText)$/;
2232
- function mergeFields(winner, loser) {
2233
- if (!winner.text && loser.text) {
2234
- winner.text = loser.text;
2235
- winner.hasLabel = winner.text.length > 0;
2236
- }
2237
- if (!winner.testID && loser.testID) winner.testID = loser.testID;
2238
- if (!winner.accessibilityLabel && loser.accessibilityLabel) winner.accessibilityLabel = loser.accessibilityLabel;
2239
- return winner;
2240
- }
2241
- var deduped = {};
2242
- for (var di = 0; di < elements.length; di++) {
2243
- var el = elements[di];
2244
- var key = el.frame.x + ',' + el.frame.y + ',' + el.frame.width + ',' + el.frame.height;
2245
- var existing = deduped[key];
2246
- if (!existing) {
2247
- deduped[key] = el;
2248
- } else {
2249
- var existingIsGeneric = HOST_NAMES.test(existing.component);
2250
- var newIsGeneric = HOST_NAMES.test(el.component);
2251
- var winner, loser;
2252
- if (existingIsGeneric && !newIsGeneric) {
2253
- winner = el; loser = existing;
2254
- } else if (!existingIsGeneric && newIsGeneric) {
2255
- winner = existing; loser = el;
2256
- } else {
2257
- // Both generic or both meaningful — prefer the one with more identifiers.
2258
- if (!existing.testID && el.testID) { winner = el; loser = existing; }
2259
- else if (!existing.accessibilityLabel && el.accessibilityLabel) { winner = el; loser = existing; }
2260
- else { winner = existing; loser = el; }
2261
- }
2262
- deduped[key] = mergeFields(winner, loser);
2263
- }
2264
- }
2265
- elements = [];
2266
- for (var dk in deduped) {
2267
- elements.push(deduped[dk]);
2268
- }
2269
-
2270
- // Tag wrappers: pressables that cover >=50% of viewport AND geometrically contain another pressable.
2271
- // These are typically keyboard-dismiss/full-screen Touchable wrappers — agents should skip them.
2272
- var viewportArea = (viewportW > 0 && viewportH > 0 && viewportW < 9999 && viewportH < 9999)
2273
- ? viewportW * viewportH : 0;
2274
- if (viewportArea > 0) {
2275
- for (var wi = 0; wi < elements.length; wi++) {
2276
- var we = elements[wi];
2277
- var weArea = we.frame.width * we.frame.height;
2278
- if (weArea < viewportArea * 0.5) continue;
2279
- for (var wj = 0; wj < elements.length; wj++) {
2280
- if (wj === wi) continue;
2281
- var other = elements[wj];
2282
- if (other.frame.x >= we.frame.x &&
2283
- other.frame.y >= we.frame.y &&
2284
- other.frame.x + other.frame.width <= we.frame.x + we.frame.width &&
2285
- other.frame.y + other.frame.height <= we.frame.y + we.frame.height &&
2286
- other.frame.width * other.frame.height < weArea) {
2287
- we.isWrapper = true;
2288
- break;
2289
- }
2290
- }
2291
- }
2292
- }
2293
-
2294
- // Build a normalized list of visible standalone texts (content + frame)
2295
- var textBoxes = [];
2296
- for (var tmi = 0; tmi < textMeasurements.length; tmi++) {
2297
- var tm = textMeasurements[tmi];
2298
- var tc = textContents[tmi];
2299
- if (!tm || !tc) continue;
2300
- if (tm.width <= 0 || tm.height <= 0) continue;
2301
- if (tm.x + tm.width < 0 || tm.y + tm.height < 0) continue;
2302
- if (tm.x > viewportW || tm.y > viewportH) continue;
2303
- textBoxes.push({
2304
- text: tc,
2305
- x: tm.x, y: tm.y, width: tm.width, height: tm.height,
2306
- cx: tm.x + tm.width / 2, cy: tm.y + tm.height / 2
2307
- });
2308
- }
2309
-
2310
- // Humanize component name → intent (strip Svg/Icon prefixes, split camelCase)
2311
- function humanize(name) {
2312
- if (!name) return '';
2313
- var n = String(name);
2314
- n = n.replace(/^(Svg|Icon)/, '').replace(/(Svg|Icon)$/, '');
2315
- n = n.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
2316
- n = n.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
2317
- return n.trim().toLowerCase();
2318
- }
2319
-
2320
- // Attach nearbyText for pressables that lack their own text/label.
2321
- // Nearest text by center-to-center distance, within a reasonable radius.
2322
- var NEARBY_RADIUS = 120; // points
2323
- for (var ei = 0; ei < elements.length; ei++) {
2324
- var pe = elements[ei];
2325
- pe.intent = humanize(pe.component);
2326
- // Skip when the pressable already has its own human-readable text or a11y label.
2327
- // testID alone doesn't suppress nearbyText — the testID may be cryptic and the
2328
- // surrounding visible text is still a useful handle for an agent.
2329
- if (pe.hasLabel || pe.accessibilityLabel) continue;
2330
- if (textBoxes.length === 0) continue;
2331
-
2332
- var pcx = pe.frame.x + pe.frame.width / 2;
2333
- var pcy = pe.frame.y + pe.frame.height / 2;
2334
- var best = null;
2335
- var bestDist = Infinity;
2336
- for (var tbi = 0; tbi < textBoxes.length; tbi++) {
2337
- var tb = textBoxes[tbi];
2338
- // Skip texts contained inside this pressable (shouldn't happen — walkTexts excludes them — but defensive)
2339
- if (tb.x >= pe.frame.x && tb.y >= pe.frame.y &&
2340
- tb.x + tb.width <= pe.frame.x + pe.frame.width &&
2341
- tb.y + tb.height <= pe.frame.y + pe.frame.height) continue;
2342
- var dx = tb.cx - pcx;
2343
- var dy = tb.cy - pcy;
2344
- var d = Math.sqrt(dx * dx + dy * dy);
2345
- if (d < bestDist && d <= NEARBY_RADIUS) {
2346
- bestDist = d;
2347
- best = tb;
2348
- }
2349
- }
2350
- if (best) pe.nearbyText = best.text;
2351
- }
2352
-
2353
- // Sort top-to-bottom, left-to-right
2354
- elements.sort(function(a, b) {
2355
- if (a.center.y !== b.center.y) return a.center.y - b.center.y;
2356
- return a.center.x - b.center.x;
2357
- });
2358
-
2359
- var iconCount = 0;
2360
- var labeledCount = 0;
2361
- for (var j = 0; j < elements.length; j++) {
2362
- if (elements[j].hasLabel) labeledCount++;
2363
- else iconCount++;
2364
- }
2365
-
2366
- return {
2367
- pressableElements: elements,
2368
- summary: 'Found ' + elements.length + ' pressable elements (' + iconCount + ' icon-only, ' + labeledCount + ' with text labels)'
2369
- };
2370
- })()
2371
- `;
2372
- const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
2373
- if (result.success && result.result) {
2374
- try {
2375
- const parsed = JSON.parse(result.result);
2376
- if (parsed.error)
2377
- return { success: false, error: parsed.error };
2378
- const pressableElements = parsed.pressableElements || [];
2379
- // Format as readable text
2380
- const lines = [parsed.summary, ""];
2381
- for (let i = 0; i < pressableElements.length; i++) {
2382
- const el = pressableElements[i];
2383
- const num = i + 1;
2384
- const label = el.hasLabel
2385
- ? `"${el.text}"`
2386
- : el.intent
2387
- ? `(${el.intent} icon)`
2388
- : "(icon/image)";
2389
- const ids = [];
2390
- if (el.testID)
2391
- ids.push(`testID="${el.testID}"`);
2392
- if (el.accessibilityLabel)
2393
- ids.push(`a11y="${el.accessibilityLabel}"`);
2394
- const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
2395
- const inputStr = el.isInput ? " (input)" : "";
2396
- const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
2397
- const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
2398
- lines.push(`${num}. ${el.component} ${label}${nearPart} — center:(${el.center.x},${el.center.y}) frame:(${el.frame.x},${el.frame.y} ${el.frame.width}x${el.frame.height})${idStr}${inputStr}${wrapperStr}`);
2399
- if (el.path)
2400
- lines.push(` path: ${el.path}`);
2401
- }
2402
- return {
2403
- success: true,
2404
- result: lines.join("\n"),
2405
- parsedElements: pressableElements
2406
- };
2407
- }
2408
- catch {
2409
- // If parsing fails, return original result
2410
- }
2411
- }
2412
- return result;
2413
- }
2414
- /**
2415
- * Format enriched elements as an indented tree with tap coordinates in pixels.
2416
- * Same tree structure as get_screen_layout but with tap:(x,y) per node.
2417
- */
2418
- function formatEnrichedLayoutTree(elements, offScreen) {
2419
- const tree = formatLayoutTree(elements, (el, indent, isLeaf) => {
2420
- const prefix = " ".repeat(indent);
2421
- const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
2422
- const tap = ` tap:(${el.tapX},${el.tapY})`;
2423
- const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
2424
- const idStr = id ? ` [${id}]` : "";
2425
- const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
2426
- return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
2427
- });
2428
- const suffix = [];
2429
- if (offScreen?.offScreenAbove?.length) {
2430
- suffix.push(formatOffScreenLine(offScreen.offScreenAbove, "above fold"));
2431
- }
2432
- if (offScreen?.offScreenBelow?.length) {
2433
- suffix.push(formatOffScreenLine(offScreen.offScreenBelow, "below fold"));
2434
- }
2435
- return suffix.length > 0 ? `${tree}\n\n${suffix.join("\n")}` : tree;
2436
- }
2437
- /**
2438
- * Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
2439
- * Converts points/dp frame coordinates to pixels using the device pixel ratio,
2440
- * computes center-point tapX/tapY for each element.
2441
- *
2442
- * @param pixelRatio - device pixel ratio (e.g., 3 for @3x iPhone)
2443
- * @param screenshotScaleFactor - if the screenshot image was scaled down, this factor adjusts coordinates
2444
- * @param device - optional target device name
2445
- * @returns formatted tree string with pixel coordinates, or null if unavailable
2446
- */
2447
- export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device, safeAreaTopPoints = 0) {
2448
- try {
2449
- const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
2450
- if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
2451
- return null;
2452
- const elements = result.parsedElements.map((el) => {
2453
- const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
2454
- // React-native-screens modal/sheet presentations on iOS cause measureInWindow to return y
2455
- // relative to the screen's content origin (below safe-area inset), not the window origin.
2456
- // An element whose center y sits inside the safe-area band is physically impossible for a
2457
- // visible interactive target — treat that as a sign of the shifted space and add the inset.
2458
- const centerXPoints = frame.x + frame.width / 2;
2459
- let centerYPoints = frame.y + frame.height / 2;
2460
- let yPoints = frame.y;
2461
- if (safeAreaTopPoints > 0 && centerYPoints < safeAreaTopPoints) {
2462
- centerYPoints += safeAreaTopPoints;
2463
- yPoints += safeAreaTopPoints;
2464
- }
2465
- const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
2466
- const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
2467
- const pixelFrame = {
2468
- x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
2469
- y: Math.round((yPoints * pixelRatio) / screenshotScaleFactor),
2470
- width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
2471
- height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
2472
- };
2473
- return {
2474
- component: el.component,
2475
- frame: pixelFrame,
2476
- tapX,
2477
- tapY,
2478
- text: el.text,
2479
- identifiers: el.identifiers,
2480
- parentIndex: el.parentIndex,
2481
- originalIndex: el.originalIndex,
2482
- depth: el.depth,
2483
- path: el.path,
2484
- };
2485
- });
2486
- return formatEnrichedLayoutTree(elements, {
2487
- offScreenBelow: result.offScreenBelow,
2488
- offScreenAbove: result.offScreenAbove,
2489
- });
2490
- }
2491
- catch {
2492
- return null; // Non-fatal: screenshot works without layout
2493
- }
2494
- }
2495
- /**
2496
- * Inspect a specific component by name, returning its props, state, and layout.
2497
- */
2498
- export async function inspectComponent(componentName, options = {}) {
2499
- const { index = 0, includeState = true, includeChildren = false, childrenDepth = 1, shortPath = true, simplifyHooks = true, device } = options;
2500
- const escapedName = componentName.replace(/'/g, "\\'");
2501
- const expression = `
2502
- (function() {
2503
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2504
- if (!hook) return { error: 'React DevTools hook not found.' };
2505
-
2506
- let roots = [];
2507
- if (hook.getFiberRoots) {
2508
- roots = [...(hook.getFiberRoots(1) || [])];
2509
- }
2510
- if (roots.length === 0 && hook.renderers) {
2511
- for (const [id] of hook.renderers) {
2512
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
2513
- if (r.length > 0) { roots = r; break; }
2514
- }
2515
- }
2516
- if (roots.length === 0) return { error: 'No fiber roots found.' };
2517
-
2518
- const targetName = '${escapedName}';
2519
- const targetIndex = ${index};
2520
- const includeState = ${includeState};
2521
- const includeChildren = ${includeChildren};
2522
- const childrenDepth = ${childrenDepth};
2523
- const shortPath = ${shortPath};
2524
- const simplifyHooks = ${simplifyHooks};
2525
- const pathSegments = 3;
2526
-
2527
- function getComponentName(fiber) {
2528
- if (!fiber || !fiber.type) return null;
2529
- if (typeof fiber.type === 'string') return fiber.type;
2530
- return fiber.type.displayName || fiber.type.name || null;
2531
- }
2532
-
2533
- function formatPath(pathArray) {
2534
- if (!shortPath || pathArray.length <= pathSegments) {
2535
- return pathArray.join(' > ');
2536
- }
2537
- return '... > ' + pathArray.slice(-pathSegments).join(' > ');
2538
- }
2539
-
2540
- function extractStyles(style) {
2541
- try {
2542
- if (!style) return null;
2543
- const merged = Array.isArray(style)
2544
- ? Object.assign({}, ...style.filter(Boolean).map(s => {
2545
- try { return typeof s === 'object' ? s : {}; }
2546
- catch { return {}; }
2547
- }))
2548
- : (typeof style === 'object' ? style : {});
2549
- return Object.keys(merged).length > 0 ? merged : null;
2550
- } catch { return { _note: '[Contains animated styles]' }; }
2551
- }
2552
-
2553
- function serializeValue(val, depth = 0) {
2554
- if (depth > 3) return '[Max depth]';
2555
- if (val === null) return null;
2556
- if (val === undefined) return undefined;
2557
- if (typeof val === 'function') return '[Function]';
2558
- if (typeof val !== 'object') return val;
2559
- if (Array.isArray(val)) {
2560
- if (val.length > 10) return '[Array(' + val.length + ')]';
2561
- return val.map(v => serializeValue(v, depth + 1));
2562
- }
2563
- // Object
2564
- const keys = Object.keys(val);
2565
- if (keys.length > 20) return '[Object(' + keys.length + ' keys)]';
2566
- const result = {};
2567
- for (const k of keys) {
2568
- try {
2569
- result[k] = serializeValue(val[k], depth + 1);
2570
- } catch {
2571
- result[k] = '[Animated Value]';
2572
- }
2573
- }
2574
- return result;
2575
- }
2576
-
2577
- function getChildTree(fiber, depth) {
2578
- if (!fiber || depth <= 0) return null;
2579
- const children = [];
2580
- let child = fiber?.child;
2581
- while (child && children.length < 30) {
2582
- const name = getComponentName(child);
2583
- if (name) {
2584
- if (depth === 1) {
2585
- // Just names for depth 1
2586
- children.push(name);
2587
- } else {
2588
- // Tree structure for depth > 1
2589
- const nestedChildren = getChildTree(child, depth - 1);
2590
- children.push(nestedChildren ? { component: name, children: nestedChildren } : name);
2591
- }
2592
- }
2593
- child = child.sibling;
2594
- }
2595
- return children.length > 0 ? children : null;
2596
- }
2597
-
2598
- const matches = [];
2599
-
2600
- function findComponent(fiber, path) {
2601
- if (!fiber) return;
2602
-
2603
- const name = getComponentName(fiber);
2604
- if (name === targetName) {
2605
- matches.push({ fiber, path: [...path, name] });
2606
- }
2607
-
2608
- let child = fiber.child;
2609
- while (child) {
2610
- const childName = getComponentName(child);
2611
- findComponent(child, childName ? [...path, childName] : path);
2612
- child = child.sibling;
2613
- }
2614
- }
2615
-
2616
- findComponent(roots[0].current, []);
2617
-
2618
- if (matches.length === 0) {
2619
- return { error: 'Component "' + targetName + '" not found in the component tree.' };
2620
- }
2621
-
2622
- if (targetIndex >= matches.length) {
2623
- return { error: 'Component "' + targetName + '" found ' + matches.length + ' times, but index ' + targetIndex + ' requested.' };
2624
- }
2625
-
2626
- const { fiber, path } = matches[targetIndex];
2627
-
2628
- const result = {
2629
- component: targetName,
2630
- path: formatPath(path),
2631
- instancesFound: matches.length,
2632
- instanceIndex: targetIndex
2633
- };
2634
-
2635
- // Props (excluding children)
2636
- if (fiber.memoizedProps) {
2637
- const props = {};
2638
- for (const key of Object.keys(fiber.memoizedProps)) {
2639
- if (key === 'children') continue;
2640
- try {
2641
- props[key] = serializeValue(fiber.memoizedProps[key]);
2642
- } catch {
2643
- props[key] = '[Animated Value]';
2644
- }
2645
- }
2646
- result.props = props;
2647
- }
2648
-
2649
- // Style separately for clarity
2650
- try {
2651
- if (fiber.memoizedProps?.style) {
2652
- result.style = extractStyles(fiber.memoizedProps.style);
2653
- }
2654
- } catch {
2655
- result.style = { _note: '[Contains animated styles]' };
2656
- }
2657
-
2658
- // State (for hooks, this is a linked list)
2659
- if (includeState && fiber.memoizedState) {
2660
- // Simplified hook value serialization
2661
- function serializeHookValue(val, depth = 0) {
2662
- try {
2663
- if (depth > 2) return '[...]';
2664
- if (val === null || val === undefined) return val;
2665
- if (typeof val === 'function') return '[Function]';
2666
- if (typeof val !== 'object') return val;
2667
- // Skip React internal structures (effects, refs with destroy/create)
2668
- if (val.create && val.destroy !== undefined) return '[Effect]';
2669
- if (val.inst && val.deps) return '[Effect]';
2670
- if (val.current !== undefined && Object.keys(val).length === 1) {
2671
- // Ref object - just show current value
2672
- return { current: serializeHookValue(val.current, depth + 1) };
2673
- }
2674
- if (Array.isArray(val)) {
2675
- if (val.length > 5) return '[Array(' + val.length + ')]';
2676
- return val.slice(0, 5).map(v => serializeHookValue(v, depth + 1));
2677
- }
2678
- const keys = Object.keys(val);
2679
- if (keys.length > 10) return '[Object(' + keys.length + ' keys)]';
2680
- const result = {};
2681
- for (const k of keys.slice(0, 10)) {
2682
- try {
2683
- result[k] = serializeHookValue(val[k], depth + 1);
2684
- } catch {
2685
- result[k] = '[Animated Value]';
2686
- }
2687
- }
2688
- return result;
2689
- } catch { return '[Animated Value]'; }
2690
- }
2691
-
2692
- // For function components with hooks
2693
- const states = [];
2694
- let state = fiber.memoizedState;
2695
- let hookIndex = 0;
2696
- while (state && hookIndex < 20) {
2697
- if (state.memoizedState !== undefined) {
2698
- const hookVal = simplifyHooks
2699
- ? serializeHookValue(state.memoizedState)
2700
- : serializeValue(state.memoizedState);
2701
- // Skip effect hooks in simplified mode
2702
- if (!simplifyHooks || (hookVal !== '[Effect]' && hookVal !== undefined)) {
2703
- states.push({
2704
- hookIndex,
2705
- value: hookVal
2706
- });
2707
- }
2708
- }
2709
- state = state.next;
2710
- hookIndex++;
2711
- }
2712
- if (states.length > 0) result.hooks = states;
2713
-
2714
- // For class components, memoizedState is the state object directly
2715
- if (states.length === 0 && typeof fiber.memoizedState === 'object') {
2716
- result.state = serializeValue(fiber.memoizedState);
2717
- }
2718
- }
2719
-
2720
- // Children tree (depth controlled by childrenDepth)
2721
- if (includeChildren) {
2722
- result.children = getChildTree(fiber, childrenDepth);
2723
- }
2724
-
2725
- return result;
2726
- })()
2727
- `;
2728
- return executeInApp(expression, false, {}, device);
2729
- }
2730
- /**
2731
- * Find all components matching a name pattern and return summary info.
2732
- */
2733
- export async function findComponents(pattern, options = {}) {
2734
- const { maxResults = 20, includeLayout = false, shortPath = true, summary = false, format = "tonl", device } = options;
2735
- const escapedPattern = pattern.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
2736
- const expression = `
2737
- (function() {
2738
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2739
- if (!hook) return { error: 'React DevTools hook not found.' };
2740
-
2741
- let roots = [];
2742
- if (hook.getFiberRoots) {
2743
- roots = [...(hook.getFiberRoots(1) || [])];
2744
- }
2745
- if (roots.length === 0 && hook.renderers) {
2746
- for (const [id] of hook.renderers) {
2747
- const r = hook.getFiberRoots ? [...(hook.getFiberRoots(id) || [])] : [];
2748
- if (r.length > 0) { roots = r; break; }
2749
- }
2750
- }
2751
- if (roots.length === 0) return { error: 'No fiber roots found.' };
2752
-
2753
- const pattern = '${escapedPattern}';
2754
- const regex = new RegExp(pattern, 'i');
2755
- const maxResults = ${maxResults};
2756
- const includeLayout = ${includeLayout};
2757
- const shortPath = ${shortPath};
2758
- const summaryMode = ${summary};
2759
- const pathSegments = 3;
2760
-
2761
- function getComponentName(fiber) {
2762
- if (!fiber || !fiber.type) return null;
2763
- if (typeof fiber.type === 'string') return fiber.type;
2764
- return fiber.type.displayName || fiber.type.name || null;
2765
- }
2766
-
2767
- function formatPath(pathArray) {
2768
- if (!shortPath || pathArray.length <= pathSegments) {
2769
- return pathArray.join(' > ');
2770
- }
2771
- return '... > ' + pathArray.slice(-pathSegments).join(' > ');
2772
- }
2773
-
2774
- function extractLayoutStyles(style) {
2775
- try {
2776
- if (!style) return null;
2777
- const merged = Array.isArray(style)
2778
- ? Object.assign({}, ...style.filter(Boolean).map(s => {
2779
- try { return typeof s === 'object' ? s : {}; }
2780
- catch { return {}; }
2781
- }))
2782
- : (typeof style === 'object' ? style : {});
2783
-
2784
- const layout = {};
2785
- const keys = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
2786
- 'paddingHorizontal', 'paddingVertical', 'margin', 'marginTop', 'marginBottom',
2787
- 'marginLeft', 'marginRight', 'marginHorizontal', 'marginVertical',
2788
- 'width', 'height', 'flex', 'flexDirection', 'justifyContent', 'alignItems'];
2789
- for (const k of keys) {
2790
- if (merged[k] !== undefined) layout[k] = merged[k];
2791
- }
2792
- return Object.keys(layout).length > 0 ? layout : null;
2793
- } catch { return null; }
2794
- }
2795
-
2796
- const results = [];
2797
-
2798
- function search(fiber, path, depth) {
2799
- if (!fiber || results.length >= maxResults) return;
2800
-
2801
- try {
2802
- var name = getComponentName(fiber);
2803
- if (name && regex.test(name)) {
2804
- var entry = {
2805
- component: name,
2806
- path: formatPath(path),
2807
- depth
2808
- };
2809
-
2810
- if (fiber.memoizedProps && fiber.memoizedProps.testID) entry.testID = fiber.memoizedProps.testID;
2811
- if (fiber.key) entry.key = fiber.key;
2812
-
2813
- if (includeLayout && fiber.memoizedProps && fiber.memoizedProps.style) {
2814
- try {
2815
- var layout = extractLayoutStyles(fiber.memoizedProps.style);
2816
- if (layout) entry.layout = layout;
2817
- } catch(e) {}
2818
- }
2819
-
2820
- results.push(entry);
2821
- }
2822
-
2823
- var child = fiber.child;
2824
- while (child && results.length < maxResults) {
2825
- var childName = getComponentName(child);
2826
- search(child, childName ? path.concat([childName]) : path, depth + 1);
2827
- child = child.sibling;
2828
- }
2829
- } catch(e) {
2830
- try {
2831
- var child = fiber.child;
2832
- while (child && results.length < maxResults) {
2833
- search(child, path, depth + 1);
2834
- child = child.sibling;
2835
- }
2836
- } catch(e2) {}
2837
- }
2838
- }
2839
-
2840
- search(roots[0].current, [], 0);
2841
-
2842
- if (summaryMode) {
2843
- const counts = {};
2844
- for (const r of results) {
2845
- counts[r.component] = (counts[r.component] || 0) + 1;
2846
- }
2847
- const sorted = Object.entries(counts)
2848
- .sort((a, b) => b[1] - a[1])
2849
- .map(([name, count]) => ({ component: name, count }));
2850
- return {
2851
- pattern,
2852
- totalMatches: results.length,
2853
- uniqueComponents: sorted.length,
2854
- components: sorted
2855
- };
2856
- }
2857
-
2858
- return {
2859
- pattern,
2860
- found: results.length,
2861
- components: results
2862
- };
2863
- })()
2864
- `;
2865
- const result = await executeInApp(expression, false, {}, device);
2866
- if (format === "tonl" && result.success && result.result) {
2867
- try {
2868
- const parsed = JSON.parse(result.result);
2869
- if (parsed.components) {
2870
- if (parsed.totalMatches !== undefined) {
2871
- const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
2872
- return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
2873
- }
2874
- else {
2875
- const tonl = formatFoundComponentsToTonl(parsed.components);
2876
- return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
2877
- }
2878
- }
2879
- }
2880
- catch {
2881
- // If parsing fails, return original result
2882
- }
2883
- }
2884
- return result;
2885
- }
2886
- // ============================================================================
2887
- // Press Element (invoke onPress via React Fiber Tree)
2888
- // ============================================================================
2889
- /**
2890
- * Find a pressable element in the React fiber tree and invoke its onPress handler.
2891
- * Matches by text content, testID, or component name.
2892
- */
2893
- export async function pressElement(options) {
2894
- const { text, testID, component, index = 0, maxTraversalDepth = 15 } = options;
2895
- if (!text && !testID && !component) {
2896
- return { success: false, error: "At least one of text, testID, or component must be provided." };
2897
- }
2898
- const esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2899
- const textParam = text ? `'${esc(text)}'` : "null";
2900
- const testIDParam = testID ? `'${esc(testID)}'` : "null";
2901
- const componentParam = component ? `'${esc(component)}'` : "null";
2902
- // --- Step 1: Walk fiber tree, collect pressable/input elements, dispatch measureInWindow ---
2903
- const dispatchExpression = `
2904
- (function() {
2905
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2906
- if (!hook) return { error: 'React DevTools hook not found. Ensure app is running in __DEV__ mode.' };
2907
-
2908
- var roots = [];
2909
- if (hook.getFiberRoots) {
2910
- roots = Array.from(hook.getFiberRoots(1) || []);
2911
- }
2912
- if (roots.length === 0 && hook.renderers) {
2913
- for (var entry of hook.renderers) {
2914
- var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
2915
- if (r.length > 0) { roots = r; break; }
2916
- }
2917
- }
2918
- if (roots.length === 0) return { error: 'No fiber roots found. Is a React Native app mounted?' };
2919
-
2920
- var searchText = ${textParam};
2921
- var searchTestID = ${testIDParam};
2922
- var searchComponent = ${componentParam};
2923
- var targetIndex = ${index};
2924
- var maxTraversalUp = ${maxTraversalDepth};
2925
-
2926
- function getComponentName(fiber) {
2927
- if (!fiber || !fiber.type) return null;
2928
- if (typeof fiber.type === 'string') return fiber.type;
2929
- return fiber.type.displayName || fiber.type.name || null;
2930
- }
2931
-
2932
- // When a fiber holds a string/number child via memoizedProps.children, return it
2933
- // without recursing — Text > RCTText > NativeText all carry the same string,
2934
- // and walking through every layer duplicates it (e.g. "CircularsCircularsCirculars").
2935
- function extractText(fiber, depth) {
2936
- if (!fiber || depth > 5000) return '';
2937
- var props = fiber.memoizedProps;
2938
- if (props) {
2939
- var ch = props.children;
2940
- if (typeof ch === 'string') return ch;
2941
- if (typeof ch === 'number') return String(ch);
2942
- if (Array.isArray(ch)) {
2943
- var allPrimitive = ch.length > 0;
2944
- var inline = [];
2945
- for (var i = 0; i < ch.length; i++) {
2946
- if (typeof ch[i] === 'string') inline.push(ch[i]);
2947
- else if (typeof ch[i] === 'number') inline.push(String(ch[i]));
2948
- else { allPrimitive = false; }
2949
- }
2950
- if (allPrimitive && inline.length > 0) return inline.join('');
2951
- }
2952
- }
2953
- var parts = [];
2954
- var child = fiber.child;
2955
- while (child) {
2956
- var t = extractText(child, depth + 1);
2957
- if (t) parts.push(t);
2958
- child = child.sibling;
2959
- }
2960
- return parts.join(' ');
2961
- }
2962
-
2963
- function getMeasurable(fiber) {
2964
- var sn = fiber.stateNode;
2965
- if (!sn) return null;
2966
- if (typeof sn.measureInWindow === 'function') return sn;
2967
- if (sn.canonical && sn.canonical.publicInstance &&
2968
- typeof sn.canonical.publicInstance.measureInWindow === 'function') {
2969
- return sn.canonical.publicInstance;
2970
- }
2971
- // Fabric leaf nodes like RCTText have no publicInstance. Measure via
2972
- // the native Fabric UIManager using the shadow node instead — needed
2973
- // for text bounds that are tight around the glyphs (not the scroll
2974
- // container the text happens to live inside).
2975
- if (sn.node && globalThis.nativeFabricUIManager &&
2976
- typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
2977
- var node = sn.node;
2978
- return {
2979
- measureInWindow: function(cb) {
2980
- try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
2981
- }
2982
- };
2983
- }
2984
- return null;
2985
- }
2986
-
2987
- var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|ScrollViewContext(Base)?|VirtualizedListContext(Resetter)?|TextInputContext|KeyboardAvoidingViewContext|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)$/;
2988
-
2989
- function isScreenHidden(name, props) {
2990
- if (!props) return false;
2991
- if (name === 'RNSScreen' && props['aria-hidden'] === true) return true;
2992
- if (name === 'MaybeScreen' && props.active === 0) return true;
2993
- if (name === 'SceneView' && props.focused === false) return true;
2994
- return false;
2995
- }
2996
-
2997
- function findMeaningfulAncestorName(fiber) {
2998
- var cur = fiber.return;
2999
- var depth = 0;
3000
- var fallbackName = null;
3001
- while (cur && depth < 20) {
3002
- var aname = getComponentName(cur);
3003
- if (aname && typeof cur.type !== 'string') {
3004
- if (!fallbackName) fallbackName = aname;
3005
- if (!RN_PRIMITIVES.test(aname)) return aname;
3006
- }
3007
- cur = cur.return;
3008
- depth++;
3009
- }
3010
- return fallbackName;
3011
- }
3012
-
3013
- // Walk UP collecting testID/nativeID from ancestors. Stop at screen boundaries.
3014
- function collectAncestorTestIDs(fiber, maxUp) {
3015
- var ids = [];
3016
- var cur = fiber.return;
3017
- var d = 0;
3018
- while (cur && d < maxUp) {
3019
- var cname = getComponentName(cur);
3020
- if (cname === 'RNSScreen' || cname === 'MaybeScreen' || cname === 'SceneView') break;
3021
- var cp = cur.memoizedProps;
3022
- if (cp) {
3023
- if (typeof cp.testID === 'string' && cp.testID) ids.push(cp.testID);
3024
- if (typeof cp.nativeID === 'string' && cp.nativeID) ids.push(cp.nativeID);
3025
- }
3026
- cur = cur.return;
3027
- d++;
3028
- }
3029
- return ids;
3030
- }
3031
-
3032
- // Find the first measurable host descendant of a fiber.
3033
- // For inputs, prefer TextInput-specific hosts over generic RCTView.
3034
- function findFirstHost(fiber, depth, isInput) {
3035
- if (!fiber || depth > 20) return null;
3036
- if (typeof fiber.type === 'string' && getMeasurable(fiber)) {
3037
- if (isInput) {
3038
- var hostType = typeof fiber.type === 'string' ? fiber.type : '';
3039
- if (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1) {
3040
- return fiber;
3041
- }
3042
- }
3043
- return fiber;
3044
- }
3045
- var child = fiber.child;
3046
- var fallback = null;
3047
- while (child) {
3048
- var found = findFirstHost(child, depth + 1, isInput);
3049
- if (found) {
3050
- if (isInput) {
3051
- var ft = typeof found.type === 'string' ? found.type : '';
3052
- if (ft.indexOf('TextInput') !== -1 || ft.indexOf('textinput') !== -1) {
3053
- return found;
3054
- }
3055
- if (!fallback) fallback = found;
3056
- } else {
3057
- return found;
3058
- }
3059
- }
3060
- child = child.sibling;
3061
- }
3062
- return fallback;
3063
- }
3064
-
3065
- var hostFibers = [];
3066
- var tapMeta = [];
3067
-
3068
- // Phase 1: Walk the entire tree, collect all pressable/input elements
3069
- function walkFiber(fiber, depth, path) {
3070
- if (!fiber || depth > 5000) return;
3071
- var name = getComponentName(fiber);
3072
- var props = fiber.memoizedProps;
3073
-
3074
- if (isScreenHidden(name, props)) return;
3075
-
3076
- var isPressable = props && typeof props.onPress === 'function';
3077
- var isInput = !isPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
3078
-
3079
- if (isPressable || isInput) {
3080
- var text = '';
3081
- if (isPressable) {
3082
- text = extractText(fiber, 0);
3083
- } else {
3084
- var val = typeof props.value === 'string' ? props.value : '';
3085
- var defVal = typeof props.defaultValue === 'string' ? props.defaultValue : '';
3086
- var ph = typeof props.placeholder === 'string' ? props.placeholder : '';
3087
- text = extractText(fiber, 0) || val || defVal || ph;
3088
- }
3089
- var tid = props.testID || props.nativeID || null;
3090
- var meaningful = findMeaningfulAncestorName(fiber);
3091
- var ancestorIDs = collectAncestorTestIDs(fiber, maxTraversalUp);
3092
-
3093
- var host = findFirstHost(fiber, 0, isInput);
3094
- if (host) {
3095
- hostFibers.push(host);
3096
- tapMeta.push({
3097
- name: name || '(anonymous)',
3098
- meaningfulComponentName: meaningful || null,
3099
- text: text.substring(0, 100),
3100
- testID: tid,
3101
- ancestorTestIDs: ancestorIDs,
3102
- path: path.join(' > '),
3103
- isInput: isInput,
3104
- isPressable: isPressable,
3105
- source: 'direct'
3106
- });
3107
- }
3108
- }
3109
-
3110
- var child = fiber.child;
3111
- while (child) {
3112
- var childName = getComponentName(child);
3113
- walkFiber(child, depth + 1, childName ? path.concat([childName]) : path);
3114
- child = child.sibling;
3115
- }
3116
- }
3117
-
3118
- for (var ri = 0; ri < roots.length; ri++) {
3119
- walkFiber(roots[ri].current, 0, []);
3120
- }
3121
-
3122
- // Phase 2a: testID on non-pressable wrapper — walk UP or DOWN to pressable/input.
3123
- // Skipped if Phase 1 already matched via own testID or ancestor testID.
3124
- if (searchTestID !== null) {
3125
- var hasEnrichedTestIDMatch = false;
3126
- for (var di = 0; di < tapMeta.length; di++) {
3127
- if (tapMeta[di].testID === searchTestID) { hasEnrichedTestIDMatch = true; break; }
3128
- var aids = tapMeta[di].ancestorTestIDs || [];
3129
- for (var ai = 0; ai < aids.length; ai++) {
3130
- if (aids[ai] === searchTestID) { hasEnrichedTestIDMatch = true; break; }
3131
- }
3132
- if (hasEnrichedTestIDMatch) break;
3133
- }
3134
-
3135
- if (!hasEnrichedTestIDMatch) {
3136
- function findDescendantPressable(fiber, d) {
3137
- if (!fiber || d > 10) return null;
3138
- var fp = fiber.memoizedProps;
3139
- var dIsPressable = fp && typeof fp.onPress === 'function';
3140
- var dIsInput = !dIsPressable && fp && (typeof fp.onChangeText === 'function' || typeof fp.onFocus === 'function');
3141
- if (dIsPressable || dIsInput) return { fiber: fiber, isPressable: dIsPressable, isInput: dIsInput };
3142
- var c = fiber.child;
3143
- while (c) {
3144
- var r = findDescendantPressable(c, d + 1);
3145
- if (r) return r;
3146
- c = c.sibling;
3147
- }
3148
- return null;
3149
- }
3150
-
3151
- function findByTestID2a(fiber, path) {
3152
- if (!fiber) return;
3153
- var name = getComponentName(fiber);
3154
- var props = fiber.memoizedProps;
3155
- if (isScreenHidden(name, props)) return;
3156
-
3157
- var tid = props && (props.testID || props.nativeID || null);
3158
- if (tid === searchTestID) {
3159
- var nIsPressable = props && typeof props.onPress === 'function';
3160
- var nIsInput = !nIsPressable && props && (typeof props.onChangeText === 'function' || typeof props.onFocus === 'function');
3161
-
3162
- if (nIsPressable || nIsInput) {
3163
- 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 : ''));
3164
- var host = findFirstHost(fiber, 0, nIsInput);
3165
- if (host) {
3166
- hostFibers.push(host);
3167
- tapMeta.push({
3168
- name: name || '(anonymous)',
3169
- meaningfulComponentName: findMeaningfulAncestorName(fiber) || null,
3170
- text: text.substring(0, 100),
3171
- testID: searchTestID,
3172
- ancestorTestIDs: [],
3173
- path: path.join(' > '),
3174
- isInput: nIsInput,
3175
- isPressable: nIsPressable,
3176
- source: 'testID-direct'
3177
- });
3178
- }
3179
- } else {
3180
- var foundAncestor = false;
3181
- var parent = fiber.return;
3182
- var d = 0;
3183
- while (parent && d < maxTraversalUp) {
3184
- var pp = parent.memoizedProps;
3185
- var pIsPressable = pp && typeof pp.onPress === 'function';
3186
- var pIsInput = !pIsPressable && pp && (typeof pp.onChangeText === 'function' || typeof pp.onFocus === 'function');
3187
- if (pIsPressable || pIsInput) {
3188
- 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 : ''));
3189
- var host = findFirstHost(parent, 0, pIsInput);
3190
- if (host) {
3191
- hostFibers.push(host);
3192
- tapMeta.push({
3193
- name: name || '(anonymous)',
3194
- meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
3195
- text: pText.substring(0, 100),
3196
- testID: pp.testID || pp.nativeID || searchTestID,
3197
- ancestorTestIDs: [],
3198
- path: path.join(' > '),
3199
- isInput: pIsInput,
3200
- isPressable: pIsPressable,
3201
- source: 'testID-ancestor'
3202
- });
3203
- foundAncestor = true;
3204
- }
3205
- break;
3206
- }
3207
- parent = parent.return;
3208
- d++;
3209
- }
3210
-
3211
- if (!foundAncestor) {
3212
- var desc = findDescendantPressable(fiber, 0);
3213
- if (desc) {
3214
- var dp = desc.fiber.memoizedProps;
3215
- 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 : ''));
3216
- var dhost = findFirstHost(desc.fiber, 0, desc.isInput);
3217
- if (dhost) {
3218
- hostFibers.push(dhost);
3219
- tapMeta.push({
3220
- name: getComponentName(desc.fiber) || '(anonymous)',
3221
- meaningfulComponentName: findMeaningfulAncestorName(desc.fiber) || null,
3222
- text: dText.substring(0, 100),
3223
- testID: dp.testID || dp.nativeID || searchTestID,
3224
- ancestorTestIDs: [searchTestID],
3225
- path: path.join(' > '),
3226
- isInput: desc.isInput,
3227
- isPressable: desc.isPressable,
3228
- source: 'testID-descendant'
3229
- });
3230
- }
3231
- }
3232
- }
3233
- }
3234
- }
3235
- var child = fiber.child;
3236
- while (child) {
3237
- var childName = getComponentName(child);
3238
- findByTestID2a(child, childName ? path.concat([childName]) : path);
3239
- child = child.sibling;
3240
- }
3241
- }
3242
- for (var ri2a = 0; ri2a < roots.length; ri2a++) {
3243
- findByTestID2a(roots[ri2a].current, []);
3244
- }
3245
- }
3246
- }
3247
-
3248
- // Phase 2b: component name on non-pressable node — walk UP or DOWN to pressable parent.
3249
- // Skipped if Phase 1 already matched via own name or meaningfulComponentName.
3250
- if (searchComponent !== null) {
3251
- var scLower = searchComponent.toLowerCase();
3252
- var hasEnrichedComponentMatch = false;
3253
- for (var ci = 0; ci < tapMeta.length; ci++) {
3254
- var cn = (tapMeta[ci].name || '').toLowerCase();
3255
- var cm = (tapMeta[ci].meaningfulComponentName || '').toLowerCase();
3256
- if (cn.indexOf(scLower) !== -1 || cm.indexOf(scLower) !== -1) {
3257
- hasEnrichedComponentMatch = true; break;
3258
- }
3259
- }
3260
-
3261
- if (!hasEnrichedComponentMatch) {
3262
- function findDescendantPressableOnly(fiber, d) {
3263
- if (!fiber || d > 10) return null;
3264
- var fp = fiber.memoizedProps;
3265
- if (fp && typeof fp.onPress === 'function') return fiber;
3266
- var c = fiber.child;
3267
- while (c) {
3268
- var r = findDescendantPressableOnly(c, d + 1);
3269
- if (r) return r;
3270
- c = c.sibling;
3271
- }
3272
- return null;
3273
- }
3274
-
3275
- function findByName2b(fiber, path) {
3276
- if (!fiber) return;
3277
- var name = getComponentName(fiber);
3278
- var props = fiber.memoizedProps;
3279
- if (isScreenHidden(name, props)) return;
3280
-
3281
- if (name && name.toLowerCase().indexOf(scLower) !== -1) {
3282
- var foundAncestor = false;
3283
- var parent = fiber.return;
3284
- var d = 0;
3285
- while (parent && d < maxTraversalUp) {
3286
- var pp = parent.memoizedProps;
3287
- if (pp && typeof pp.onPress === 'function') {
3288
- var text = extractText(parent, 0);
3289
- var host = findFirstHost(parent, 0, false);
3290
- if (host) {
3291
- hostFibers.push(host);
3292
- tapMeta.push({
3293
- name: name,
3294
- meaningfulComponentName: findMeaningfulAncestorName(parent) || null,
3295
- text: text.substring(0, 100),
3296
- testID: pp.testID || pp.nativeID || null,
3297
- ancestorTestIDs: [],
3298
- path: path.join(' > '),
3299
- isInput: false,
3300
- isPressable: true,
3301
- source: 'component-ancestor'
3302
- });
3303
- foundAncestor = true;
3304
- }
3305
- break;
3306
- }
3307
- parent = parent.return;
3308
- d++;
3309
- }
3310
-
3311
- if (!foundAncestor) {
3312
- var descFiber = findDescendantPressableOnly(fiber, 0);
3313
- if (descFiber) {
3314
- var dp = descFiber.memoizedProps;
3315
- var dText = extractText(descFiber, 0);
3316
- var dhost = findFirstHost(descFiber, 0, false);
3317
- if (dhost) {
3318
- hostFibers.push(dhost);
3319
- tapMeta.push({
3320
- name: getComponentName(descFiber) || '(anonymous)',
3321
- meaningfulComponentName: name,
3322
- text: dText.substring(0, 100),
3323
- testID: dp.testID || dp.nativeID || null,
3324
- ancestorTestIDs: [],
3325
- path: path.join(' > '),
3326
- isInput: false,
3327
- isPressable: true,
3328
- source: 'component-descendant'
3329
- });
3330
- }
3331
- }
3332
- }
3333
- }
3334
- var child = fiber.child;
3335
- while (child) {
3336
- var childName = getComponentName(child);
3337
- findByName2b(child, childName ? path.concat([childName]) : path);
3338
- child = child.sibling;
3339
- }
3340
- }
3341
- for (var ri2b = 0; ri2b < roots.length; ri2b++) {
3342
- findByName2b(roots[ri2b].current, []);
3343
- }
3344
- }
3345
- }
3346
-
3347
- if (hostFibers.length === 0) {
3348
- var criteria = [];
3349
- if (searchText !== null) criteria.push('text="' + searchText + '"');
3350
- if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
3351
- if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
3352
- return { error: 'No pressable or focusable elements found. Searched for: ' + criteria.join(', ') };
3353
- }
3354
-
3355
- // Store host fibers and metadata globally for step 2, dispatch measureInWindow
3356
- globalThis.__tapHostFibers = hostFibers;
3357
- globalThis.__tapMeta = tapMeta;
3358
- globalThis.__tapMeasurements = new Array(hostFibers.length).fill(null);
3359
-
3360
- for (var mi = 0; mi < hostFibers.length; mi++) {
3361
- try {
3362
- (function(idx) {
3363
- getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
3364
- globalThis.__tapMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
3365
- });
3366
- })(mi);
3367
- } catch(e) {}
3368
- }
3369
-
3370
- return { count: hostFibers.length };
3371
- })()
3372
- `;
3373
- const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, options.device);
3374
- if (!dispatchResult.success)
3375
- return dispatchResult;
3376
- try {
3377
- const parsed = JSON.parse(dispatchResult.result || "{}");
3378
- if (parsed.error)
3379
- return { success: false, error: parsed.error };
3380
- }
3381
- catch {
3382
- /* ignore */
3383
- }
3384
- // Wait for measureInWindow callbacks
3385
- await delay(300);
3386
- // --- Step 2: Read measurements, filter visible, match by query ---
3387
- const resolveExpression = `
3388
- (function() {
3389
- var hostFibers = globalThis.__tapHostFibers;
3390
- var meta = globalThis.__tapMeta;
3391
- var measurements = globalThis.__tapMeasurements;
3392
- globalThis.__tapHostFibers = null;
3393
- globalThis.__tapMeta = null;
3394
- globalThis.__tapMeasurements = null;
3395
-
3396
- if (!hostFibers || !measurements || !meta) {
3397
- return { error: 'No measurement data. Dispatch step may have failed.' };
3398
- }
3399
-
3400
- var searchText = ${textParam};
3401
- var searchTestID = ${testIDParam};
3402
- var searchComponent = ${componentParam};
3403
- var targetIndex = ${index};
3404
-
3405
- // Determine viewport bounds
3406
- var viewportW = 9999, viewportH = 9999;
3407
- for (var v = 0; v < measurements.length; v++) {
3408
- if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
3409
- measurements[v].width > 0 && measurements[v].height > 0) {
3410
- viewportW = measurements[v].width;
3411
- viewportH = measurements[v].height + measurements[v].y;
3412
- break;
3413
- }
3414
- }
3415
-
3416
- // Filter visible and match
3417
- var matches = [];
3418
- for (var i = 0; i < measurements.length; i++) {
3419
- var m = measurements[i];
3420
- if (!m) continue;
3421
-
3422
- // Visibility filter: positive dimensions, within viewport
3423
- if (m.width <= 0 || m.height <= 0) continue;
3424
- if (m.x + m.width < 0 || m.y + m.height < 0) continue;
3425
- if (m.x > viewportW || m.y > viewportH) continue;
3426
-
3427
- var info = meta[i];
3428
-
3429
- // Match by query — OR across own and enriched identifiers
3430
- var matched = true;
3431
- if (searchText !== null) {
3432
- matched = matched && info.text.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
3433
- }
3434
- if (searchTestID !== null) {
3435
- var ownTidMatch = info.testID === searchTestID;
3436
- var aTids = info.ancestorTestIDs || [];
3437
- var ancestorTidMatch = false;
3438
- for (var ti = 0; ti < aTids.length; ti++) {
3439
- if (aTids[ti] === searchTestID) { ancestorTidMatch = true; break; }
3440
- }
3441
- matched = matched && (ownTidMatch || ancestorTidMatch);
3442
- }
3443
- if (searchComponent !== null) {
3444
- var scq = searchComponent.toLowerCase();
3445
- var ownNameMatch = (info.name || '').toLowerCase().indexOf(scq) !== -1;
3446
- var meaningfulMatch = (info.meaningfulComponentName || '').toLowerCase().indexOf(scq) !== -1;
3447
- matched = matched && (ownNameMatch || meaningfulMatch);
3448
- }
3449
-
3450
- if (matched) {
3451
- matches.push({
3452
- name: info.name,
3453
- text: info.text,
3454
- testID: info.testID,
3455
- path: info.path,
3456
- isInput: info.isInput,
3457
- x: Math.round(m.x + m.width / 2),
3458
- y: Math.round(m.y + m.height / 2)
3459
- });
3460
- }
3461
- }
3462
-
3463
- if (matches.length === 0) {
3464
- var criteria = [];
3465
- if (searchText !== null) criteria.push('text="' + searchText + '"');
3466
- if (searchTestID !== null) criteria.push('testID="' + searchTestID + '"');
3467
- if (searchComponent !== null) criteria.push('component="' + searchComponent + '"');
3468
- return { error: 'No visible pressable or focusable elements found matching: ' + criteria.join(', ') };
3469
- }
3470
-
3471
- if (targetIndex >= matches.length) {
3472
- return {
3473
- error: 'Found ' + matches.length + ' visible match(es) but index ' + targetIndex + ' requested (0-based). Use index 0-' + (matches.length - 1) + '.',
3474
- matches: matches.map(function(m, i) {
3475
- return { index: i, component: m.name, text: m.text, testID: m.testID };
3476
- })
3477
- };
3478
- }
3479
-
3480
- var target = matches[targetIndex];
3481
- var result = {
3482
- needsNativeTap: true,
3483
- nativeTapTarget: { x: target.x, y: target.y, unit: 'points' },
3484
- pressed: target.name,
3485
- matchIndex: targetIndex,
3486
- totalMatches: matches.length,
3487
- text: target.text,
3488
- testID: target.testID,
3489
- path: target.path,
3490
- isInput: target.isInput
3491
- };
3492
- if (matches.length > 1) {
3493
- result.allMatches = matches.map(function(m, i) {
3494
- return { index: i, component: m.name, text: m.text, testID: m.testID, x: m.x, y: m.y };
3495
- });
3496
- }
3497
- return result;
3498
- })()
3499
- `;
3500
- return executeInApp(resolveExpression, false, { timeoutMs: 10000 }, options.device);
3501
- }
3502
- // ============================================================================
3503
- // Coordinate-Based Element Inspection (via DevTools Inspector API)
3504
- // ============================================================================
3505
- /**
3506
- * Toggle the Element Inspector via DevSettings native module.
3507
- * This enables the inspector overlay programmatically.
3508
- */
3509
- export async function toggleElementInspector(device) {
3510
- const expression = `
3511
- (function() {
3512
- const ds = globalThis.nativeModuleProxy?.DevSettings;
3513
- if (!ds) return { error: 'DevSettings not available' };
3514
-
3515
- const proto = Object.getPrototypeOf(ds);
3516
- if (!proto || typeof proto.toggleElementInspector !== 'function') {
3517
- return { error: 'toggleElementInspector not found' };
3518
- }
3519
-
3520
- try {
3521
- proto.toggleElementInspector.call(ds);
3522
- return { success: true, message: 'Element Inspector toggled' };
3523
- } catch (e) {
3524
- return { error: 'Failed to toggle: ' + e.message };
3525
- }
3526
- })()
3527
- `;
3528
- return executeInApp(expression, false, {}, device);
3529
- }
3530
- /**
3531
- * Check if the Element Inspector overlay is currently active.
3532
- */
3533
- export async function isInspectorActive(device) {
3534
- const expression = `
3535
- (function() {
3536
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3537
- if (!hook) return false;
3538
-
3539
- let roots = [...(hook.getFiberRoots?.(1) || [])];
3540
- if (roots.length === 0) {
3541
- for (const [id] of (hook.renderers || [])) {
3542
- roots = [...(hook.getFiberRoots?.(id) || [])];
3543
- if (roots.length > 0) break;
3544
- }
3545
- }
3546
- if (roots.length === 0) return false;
3547
-
3548
- function findComponent(fiber, targetName, depth = 0) {
3549
- if (!fiber || depth > 5000) return null;
3550
- const name = fiber.type?.displayName || fiber.type?.name;
3551
- if (name === targetName) return fiber;
3552
- let child = fiber.child;
3553
- while (child) {
3554
- const found = findComponent(child, targetName, depth + 1);
3555
- if (found) return found;
3556
- child = child.sibling;
3557
- }
3558
- return null;
3559
- }
3560
-
3561
- return !!findComponent(roots[0].current, 'InspectorPanel');
3562
- })()
3563
- `;
3564
- const result = await executeInApp(expression, false, {}, device);
3565
- if (result.success && result.result) {
3566
- return result.result === "true";
3567
- }
3568
- return false;
3569
- }
3570
- /**
3571
- * Get the currently selected element from the Element Inspector overlay.
3572
- * This reads the InspectorPanel component's props to get the hierarchy, frame, and style.
3573
- * Requires the Element Inspector to be enabled and an element to be selected.
3574
- */
3575
- export async function getInspectorSelection(device) {
3576
- const expression = `
3577
- (function() {
3578
- const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3579
- if (!hook) return { error: 'React DevTools hook not available.' };
3580
-
3581
- // Find fiber roots
3582
- let roots = [...(hook.getFiberRoots?.(1) || [])];
3583
- if (roots.length === 0) {
3584
- for (const [id] of (hook.renderers || [])) {
3585
- roots = [...(hook.getFiberRoots?.(id) || [])];
3586
- if (roots.length > 0) break;
3587
- }
3588
- }
3589
- if (roots.length === 0) return { error: 'No fiber roots found.' };
3590
-
3591
- // Find all InspectorPanel instances (apps with modals may have multiple)
3592
- function findAllPanels(fiber, targetName, depth, results) {
3593
- if (!fiber || depth > 5000) return;
3594
- const name = fiber.type?.displayName || fiber.type?.name;
3595
- if (name === targetName) results.push(fiber);
3596
- let child = fiber.child;
3597
- while (child) {
3598
- findAllPanels(child, targetName, depth + 1, results);
3599
- child = child.sibling;
3600
- }
3601
- }
3602
-
3603
- const panels = [];
3604
- findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
3605
- if (panels.length === 0) {
3606
- return {
3607
- error: 'Element Inspector is not active.',
3608
- hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
3609
- };
3610
- }
3611
-
3612
- // Prefer the panel that has an active selection
3613
- const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
3614
- const props = panelFiber.memoizedProps;
3615
- if (!props.hierarchy || props.hierarchy.length === 0) {
3616
- return {
3617
- error: 'No element selected.',
3618
- hint: 'Tap on an element in the app to select it for inspection.'
3619
- };
3620
- }
3621
-
3622
- // Build the path from hierarchy
3623
- const path = props.hierarchy.map(h => h.name).join(' > ');
3624
- const element = props.hierarchy[props.hierarchy.length - 1]?.name || 'Unknown';
3625
-
3626
- // Extract style info
3627
- let style = {};
3628
- if (props.inspected?.style) {
3629
- const styles = Array.isArray(props.inspected.style)
3630
- ? props.inspected.style
3631
- : [props.inspected.style];
3632
- for (const s of styles) {
3633
- if (s && typeof s === 'object') {
3634
- Object.assign(style, s);
3635
- }
3636
- }
3637
- }
3638
-
3639
- return {
3640
- element,
3641
- path,
3642
- frame: props.inspected?.frame || null,
3643
- style: Object.keys(style).length > 0 ? style : null,
3644
- selection: props.selection,
3645
- hierarchyLength: props.hierarchy.length
3646
- };
3647
- })()
3648
- `;
3649
- return executeInApp(expression, false, {}, device);
3650
- }
3651
- /**
3652
- * Resolve the component at (x, y) using RN's built-in Element Inspector.
3653
- *
3654
- * Strategy: programmatically toggle the inspector overlay on (if needed),
3655
- * then call InspectorOverlay.props.onTouchPoint(x, y) directly — bypassing
3656
- * the broken adb-tap → PanResponder route on Bridgeless / new-arch RN.
3657
- * This populates InspectorPanel with RN's full curated hierarchy, the
3658
- * inspected element's frame, and per-component style data (margin, padding,
3659
- * border, layout) — exactly what the on-device overlay shows.
3660
- *
3661
- * Returns the rich hierarchy with style merged from each entry's
3662
- * getInspectorData(...).props.style, plus the inspected frame and style.
3663
- *
3664
- * Auto-hides the overlay after capture so subsequent screenshots stay clean.
3665
- */
3666
- export async function getInspectorSelectionAtPoint(x, y, device) {
3667
- // Step 1: ensure the inspector overlay is mounted (toggle on if currently off).
3668
- const setupExpression = `
3669
- (function() {
3670
- var ds = globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.DevSettings;
3671
- if (!ds) return { error: 'DevSettings native module not available.' };
3672
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3673
- if (!hook) return { error: 'React DevTools hook not available. Make sure you are running a development build.' };
3674
-
3675
- function findOverlays() {
3676
- var roots = [];
3677
- if (hook.renderers) {
3678
- for (var entry of hook.renderers) {
3679
- try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch(e) {}
3680
- }
3681
- }
3682
- var overlays = [];
3683
- function walk(f, d) {
3684
- if (!f || d > 800) return;
3685
- if (f.type && typeof f.type !== 'string' &&
3686
- (f.type.displayName || f.type.name) === 'InspectorOverlay') {
3687
- overlays.push(f);
3688
- }
3689
- var c = f.child;
3690
- while (c) { walk(c, d + 1); c = c.sibling; }
3691
- }
3692
- for (var r of roots) { walk(r.current, 0); }
3693
- return overlays;
3694
- }
3695
-
3696
- var overlays = findOverlays();
3697
- // Two overlays expected when active: one per fiber root (LogBox + app).
3698
- var wasActive = overlays.length >= 2;
3699
- if (!wasActive) {
3700
- var proto = Object.getPrototypeOf(ds);
3701
- if (typeof proto.toggleElementInspector !== 'function') {
3702
- return { error: 'toggleElementInspector not found on DevSettings.' };
3703
- }
3704
- proto.toggleElementInspector.call(ds);
3705
- }
3706
- return { wasActive: wasActive };
3707
- })()
3708
- `;
3709
- const setup = await executeInApp(setupExpression, false, {}, device);
3710
- if (!setup.success)
3711
- return setup;
3712
- try {
3713
- const parsed = JSON.parse(setup.result || "{}");
3714
- if (parsed.error)
3715
- return { success: false, error: parsed.error };
3716
- }
3717
- catch {
3718
- /* ignore */
3719
- }
3720
- // Allow the inspector overlay to mount.
3721
- await new Promise((resolve) => setTimeout(resolve, 350));
3722
- // Step 2: locate the app InspectorOverlay (skip LogBox at index 0) and dispatch onTouchPoint.
3723
- const tapExpression = `
3724
- (function() {
3725
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3726
- var roots = [];
3727
- for (var entry of hook.renderers) {
3728
- try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch(e) {}
3729
- }
3730
- var overlays = [];
3731
- function walk(f, d) {
3732
- if (!f || d > 800) return;
3733
- if (f.type && typeof f.type !== 'string' &&
3734
- (f.type.displayName || f.type.name) === 'InspectorOverlay') {
3735
- overlays.push(f);
3736
- }
3737
- var c = f.child;
3738
- while (c) { walk(c, d + 1); c = c.sibling; }
3739
- }
3740
- for (var r of roots) { walk(r.current, 0); }
3741
-
3742
- // The LogBox renderer's overlay is mounted first; the app overlay is the last one.
3743
- var overlay = overlays[overlays.length - 1];
3744
- if (!overlay || !overlay.memoizedProps || typeof overlay.memoizedProps.onTouchPoint !== 'function') {
3745
- return { error: 'InspectorOverlay.onTouchPoint unavailable. Inspector may not be mounted.' };
3746
- }
3747
- try {
3748
- overlay.memoizedProps.onTouchPoint(${x}, ${y});
3749
- } catch (e) {
3750
- return { error: 'onTouchPoint failed: ' + (e && e.message || String(e)) };
3751
- }
3752
- return { ok: true };
3753
- })()
3754
- `;
3755
- const tap = await executeInApp(tapExpression, false, {}, device);
3756
- if (!tap.success)
3757
- return tap;
3758
- try {
3759
- const parsed = JSON.parse(tap.result || "{}");
3760
- if (parsed.error)
3761
- return { success: false, error: parsed.error };
3762
- }
3763
- catch {
3764
- /* ignore */
3765
- }
3766
- // Allow setState/render to propagate before reading panel props.
3767
- await new Promise((resolve) => setTimeout(resolve, 250));
3768
- // Step 3: read the populated InspectorPanel and shape the response.
3769
- const readExpression = `
3770
- (function() {
3771
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3772
- var roots = [];
3773
- for (var entry of hook.renderers) {
3774
- try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch(e) {}
3775
- }
3776
- var panels = [];
3777
- function walk(f, d) {
3778
- if (!f || d > 800) return;
3779
- if (f.type && typeof f.type !== 'string' &&
3780
- (f.type.displayName || f.type.name) === 'InspectorPanel') {
3781
- panels.push(f);
3782
- }
3783
- var c = f.child;
3784
- while (c) { walk(c, d + 1); c = c.sibling; }
3785
- }
3786
- for (var r of roots) { walk(r.current, 0); }
3787
-
3788
- var withSel = panels.filter(function(p) {
3789
- return p.memoizedProps && p.memoizedProps.hierarchy && p.memoizedProps.hierarchy.length > 0;
3790
- });
3791
- if (withSel.length === 0) {
3792
- return { error: 'Inspector did not select an element at (${x}, ${y}). Coordinates may be outside the app bounds.' };
3793
- }
3794
- var props = withSel[withSel.length - 1].memoizedProps;
3795
-
3796
- function flattenStyle(raw) {
3797
- if (!raw) return null;
3798
- var arr = Array.isArray(raw) ? raw : [raw];
3799
- var merged = {};
3800
- for (var i = 0; i < arr.length; i++) {
3801
- var v = arr[i];
3802
- if (v && typeof v === 'object') {
3803
- var keys = Object.keys(v);
3804
- for (var k = 0; k < keys.length; k++) merged[keys[k]] = v[keys[k]];
3805
- }
3806
- }
3807
- return Object.keys(merged).length > 0 ? merged : null;
3808
- }
3809
-
3810
- var idFn = function(x) { return x; };
3811
- // Build hierarchy with style + source per entry; dedupe consecutive duplicates.
3812
- var hierarchy = [];
3813
- var prevName = null;
3814
- for (var i = 0; i < props.hierarchy.length; i++) {
3815
- var h = props.hierarchy[i];
3816
- var name = h.name;
3817
- if (!name || name === prevName) continue;
3818
- prevName = name;
3819
- var data = h.getInspectorData ? h.getInspectorData(idFn) : null;
3820
- var style = data && data.props ? flattenStyle(data.props.style) : null;
3821
- var entry = { name: name };
3822
- if (style) entry.style = style;
3823
- if (data && data.source && data.source.fileName) {
3824
- entry.source = data.source.fileName + (data.source.lineNumber ? (':' + data.source.lineNumber) : '');
3825
- }
3826
- hierarchy.push(entry);
3827
- }
3828
-
3829
- var element = hierarchy.length > 0 ? hierarchy[hierarchy.length - 1].name : 'Unknown';
3830
- var path = hierarchy.map(function(e) { return e.name; }).join(' > ') || element;
3831
-
3832
- return {
3833
- element: element,
3834
- path: path,
3835
- frame: props.inspected ? props.inspected.frame : null,
3836
- style: props.inspected ? flattenStyle(props.inspected.style) : null,
3837
- hierarchy: hierarchy,
3838
- selection: props.selection
3839
- };
3840
- })()
3841
- `;
3842
- const readResult = await executeInApp(readExpression, false, {}, device);
3843
- // Step 4: hide the overlay so it doesn't pollute subsequent screenshots.
3844
- // We always hide after a successful capture — agents don't manually toggle.
3845
- const teardownExpression = `
3846
- (function() {
3847
- var ds = globalThis.nativeModuleProxy && globalThis.nativeModuleProxy.DevSettings;
3848
- if (!ds) return { skipped: true };
3849
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3850
- if (!hook) return { skipped: true };
3851
- var roots = [];
3852
- for (var entry of hook.renderers) {
3853
- try { roots = roots.concat(Array.from(hook.getFiberRoots(entry[0]) || [])); } catch(e) {}
3854
- }
3855
- var overlays = 0;
3856
- function walk(f, d) {
3857
- if (!f || d > 800) return;
3858
- if (f.type && typeof f.type !== 'string' &&
3859
- (f.type.displayName || f.type.name) === 'InspectorOverlay') overlays++;
3860
- var c = f.child;
3861
- while (c) { walk(c, d + 1); c = c.sibling; }
3862
- }
3863
- for (var r of roots) { walk(r.current, 0); }
3864
- if (overlays >= 2) {
3865
- try { Object.getPrototypeOf(ds).toggleElementInspector.call(ds); } catch(e) {}
3866
- }
3867
- return { ok: true };
3868
- })()
3869
- `;
3870
- try {
3871
- await executeInApp(teardownExpression, false, {}, device);
3872
- }
3873
- catch {
3874
- /* best-effort hide; don't fail the call */
3875
- }
3876
- return readResult;
3877
- }
3878
- /**
3879
- * Inspect the React component at a specific (x, y) coordinate.
3880
- *
3881
- * Works on both Paper and Fabric (New Architecture). Uses a two-step approach
3882
- * because measureInWindow callbacks fire in a future native event loop tick
3883
- * (not microtasks), so awaitPromise cannot be used to collect them:
3884
- *
3885
- * Step 1 — dispatch: walk the fiber tree, call measureInWindow on each host
3886
- * component, store fiber refs and results in app globals.
3887
- * Step 2 — resolve (after 300ms): read the globals, hit-test against target
3888
- * coordinates, return the innermost matching React component.
3889
- */
3890
- export async function inspectAtPoint(x, y, options = {}) {
3891
- const { includeProps = true, includeFrame = true, device } = options;
3892
- // --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
3893
- const dispatchExpression = `
3894
- (function() {
3895
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3896
- if (!hook) return { error: 'React DevTools hook not available. Make sure you are running a development build.' };
3897
-
3898
- var roots = [];
3899
- if (hook.getFiberRoots) {
3900
- try { roots = Array.from(hook.getFiberRoots(1) || []); } catch(e) {}
3901
- }
3902
- if (roots.length === 0 && hook.renderers) {
3903
- for (var entry of hook.renderers) {
3904
- try {
3905
- var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
3906
- if (r.length > 0) { roots = r; break; }
3907
- } catch(e) {}
3908
- }
3909
- }
3910
- if (roots.length === 0) return { error: 'No fiber roots found. The app may not have rendered yet.' };
3911
-
3912
- // Paper: measureInWindow is on stateNode directly.
3913
- // Fabric: measureInWindow is on stateNode.canonical.publicInstance.
3914
- function getMeasurable(fiber) {
3915
- var sn = fiber.stateNode;
3916
- if (!sn) return null;
3917
- if (typeof sn.measureInWindow === 'function') return sn;
3918
- if (sn.canonical && sn.canonical.publicInstance &&
3919
- typeof sn.canonical.publicInstance.measureInWindow === 'function') {
3920
- return sn.canonical.publicInstance;
3921
- }
3922
- // Fabric leaf nodes like RCTText have no publicInstance. Measure via
3923
- // the native Fabric UIManager using the shadow node instead — needed
3924
- // for text bounds that are tight around the glyphs (not the scroll
3925
- // container the text happens to live inside).
3926
- if (sn.node && globalThis.nativeFabricUIManager &&
3927
- typeof globalThis.nativeFabricUIManager.measureInWindow === 'function') {
3928
- var node = sn.node;
3929
- return {
3930
- measureInWindow: function(cb) {
3931
- try { globalThis.nativeFabricUIManager.measureInWindow(node, cb); } catch(e) {}
3932
- }
3933
- };
3934
- }
3935
- return null;
3936
- }
3937
-
3938
- var hostFibers = [];
3939
- function walkFibers(fiber, depth) {
3940
- var cur = fiber;
3941
- while (cur) {
3942
- if (hostFibers.length >= 500) return;
3943
- if (typeof cur.type === 'string' && getMeasurable(cur)) hostFibers.push(cur);
3944
- if (cur.child && depth < 250) walkFibers(cur.child, depth + 1);
3945
- cur = cur.sibling;
3946
- }
3947
- }
3948
- for (var root of roots) { walkFibers(root.current, 0); }
3949
-
3950
- if (hostFibers.length === 0) return { error: 'No measurable host components found. App may not be fully rendered.' };
3951
-
3952
- globalThis.__inspectFibers = hostFibers;
3953
- globalThis.__inspectMeasurements = new Array(hostFibers.length).fill(null);
3954
-
3955
- hostFibers.forEach(function(fiber, i) {
3956
- try {
3957
- getMeasurable(fiber).measureInWindow(function(fx, fy, fw, fh) {
3958
- globalThis.__inspectMeasurements[i] = { x: fx, y: fy, width: fw, height: fh };
3959
- });
3960
- } catch(e) {}
3961
- });
3962
-
3963
- return { count: hostFibers.length };
3964
- })()
3965
- `;
3966
- const dispatchResult = await executeInApp(dispatchExpression, false, {}, device);
3967
- if (!dispatchResult.success)
3968
- return dispatchResult;
3969
- try {
3970
- const parsed = JSON.parse(dispatchResult.result || "{}");
3971
- if (parsed.error)
3972
- return { success: false, error: parsed.error };
3973
- }
3974
- catch {
3975
- /* ignore parse errors */
3976
- }
3977
- // Wait for native measureInWindow callbacks to fire
3978
- await delay(300);
3979
- // --- Step 2: read measurements, hit-test, return result ---
3980
- const resolveExpression = `
3981
- (function() {
3982
- var fibers = globalThis.__inspectFibers;
3983
- var measurements = globalThis.__inspectMeasurements;
3984
- globalThis.__inspectFibers = null;
3985
- globalThis.__inspectMeasurements = null;
3986
-
3987
- if (!fibers || !measurements) return { error: 'No measurement data available. Run inspect_at_point again.' };
3988
-
3989
- var targetX = ${x};
3990
- var targetY = ${y};
3991
-
3992
- var hits = [];
3993
- for (var i = 0; i < measurements.length; i++) {
3994
- var m = measurements[i];
3995
- if (m && m.width > 0 && m.height > 0 &&
3996
- targetX >= m.x && targetX <= m.x + m.width &&
3997
- targetY >= m.y && targetY <= m.y + m.height) {
3998
- hits.push({ fiber: fibers[i], x: m.x, y: m.y, width: m.width, height: m.height });
3999
- }
4000
- }
4001
-
4002
- if (hits.length === 0) {
4003
- return { point: { x: targetX, y: targetY }, error: 'No component found at this point. Coordinates may be outside the app bounds or over a native-only element.' };
4004
- }
4005
-
4006
- // Smallest area = innermost (most specific) component
4007
- hits.sort(function(a, b) { return (a.width * a.height) - (b.width * b.height); });
4008
- var best = hits[0];
4009
-
4010
- // RN primitives and internal components to skip when surfacing the "element" name.
4011
- // We want the nearest *custom* component, not a library wrapper.
4012
- 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.*)$/;
4013
-
4014
- function getNearestNamed(fiber, skipPrimitives) {
4015
- var cur = fiber;
4016
- var fallback = null;
4017
- while (cur) {
4018
- if (cur.type && typeof cur.type !== 'string') {
4019
- var name = cur.type.displayName || cur.type.name;
4020
- if (name) {
4021
- if (!fallback) fallback = { name: name, fiber: cur };
4022
- if (!skipPrimitives || !RN_PRIMITIVES.test(name)) {
4023
- return { name: name, fiber: cur };
4024
- }
4025
- }
4026
- }
4027
- cur = cur.return;
4028
- }
4029
- return fallback;
4030
- }
4031
-
4032
- function buildPath(fiber) {
4033
- var path = [];
4034
- var cur = fiber;
4035
- while (cur) {
4036
- if (cur.type) {
4037
- var n = typeof cur.type === 'string'
4038
- ? cur.type
4039
- : (cur.type.displayName || cur.type.name);
4040
- if (n) path.unshift(n);
4041
- }
4042
- cur = cur.return;
4043
- }
4044
- return path.slice(-8).join(' > ');
4045
- }
4046
-
4047
- // Find nearest custom component (skipping RN primitives) for the element name,
4048
- // but fall back to the nearest named component if nothing custom is found.
4049
- var named = getNearestNamed(best.fiber.return || best.fiber, true);
4050
- var result = {
4051
- point: { x: targetX, y: targetY },
4052
- element: named ? named.name : best.fiber.type,
4053
- nativeElement: best.fiber.type,
4054
- path: buildPath(best.fiber)
4055
- };
4056
-
4057
- if (${includeFrame}) {
4058
- result.frame = { x: best.x, y: best.y, width: best.width, height: best.height };
4059
- }
4060
-
4061
- if (${includeProps} && named && named.fiber.memoizedProps) {
4062
- var props = {};
4063
- var keys = Object.keys(named.fiber.memoizedProps);
4064
- for (var i = 0; i < keys.length; i++) {
4065
- var key = keys[i];
4066
- if (key === 'children') continue;
4067
- var val = named.fiber.memoizedProps[key];
4068
- if (typeof val === 'function') {
4069
- props[key] = '[Function]';
4070
- } else if (typeof val === 'object' && val !== null) {
4071
- try {
4072
- var str = JSON.stringify(val);
4073
- props[key] = str.length > 200
4074
- ? (Array.isArray(val) ? '[Array(' + val.length + ')]' : '[Object]')
4075
- : val;
4076
- } catch(e) {
4077
- props[key] = '[Object]';
4078
- }
4079
- } else {
4080
- props[key] = val;
4081
- }
4082
- }
4083
- if (Object.keys(props).length > 0) result.props = props;
4084
- }
4085
-
4086
- // Hierarchy: custom-named component for each hit, deduped, innermost→outermost
4087
- var hierarchy = [];
4088
- for (var j = 0; j < Math.min(hits.length, 15); j++) {
4089
- var n2 = getNearestNamed(hits[j].fiber.return, true) || getNearestNamed(hits[j].fiber, true);
4090
- if (n2 && !hierarchy.some(function(h) { return h.name === n2.name; })) {
4091
- hierarchy.push({
4092
- name: n2.name,
4093
- frame: { x: hits[j].x, y: hits[j].y, width: hits[j].width, height: hits[j].height }
4094
- });
4095
- }
4096
- }
4097
- if (hierarchy.length > 1) result.hierarchy = hierarchy;
4098
-
4099
- return result;
4100
- })()
4101
- `;
4102
- return executeInApp(resolveExpression, false, {}, device);
4103
- }
1
+ // Barrel module — all symbols formerly defined here have been split into
2
+ // focused modules. Keep this file as a thin re-export so existing imports
3
+ // (e.g. src/__tests__/unit/executor.test.ts) continue to work.
4
+ export * from "./jsExecute.js";
5
+ export * from "./debugGlobals.js";
6
+ export * from "./componentTree.js";
7
+ export * from "./screenLayout.js";
8
+ export * from "./pressables.js";
9
+ export * from "./componentSearch.js";
10
+ export * from "./inspector.js";
4104
11
  //# sourceMappingURL=executor.js.map