@houwert/conductor 0.15.0 → 0.17.0

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.
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+ /**
3
+ * JS payloads injected into the RN runtime via `Runtime.evaluate`.
4
+ *
5
+ * These scripts mirror the approach in software-mansion/argent:
6
+ * - Detect Fabric (`nativeFabricUIManager`) vs Paper (`UIManager` via `__r`).
7
+ * - For component-tree: walk the fiber tree, filter wrappers via a SKIP set,
8
+ * batch-measure on-screen rects via Paper/Fabric measure APIs, return JSON
9
+ * via the `__conductor_callback` binding keyed on `requestId`.
10
+ * - For inspect-element: use `renderer.rendererConfig.getInspectorDataForViewAtPoint`
11
+ * (React's own inspector) and walk UP via `.return` from `data.closestInstance`.
12
+ *
13
+ * Both scripts return a small ack value synchronously and post the real result
14
+ * asynchronously through the binding — that's why the caller uses
15
+ * `MetroCdpClient.installCallbackBinding`.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.makeComponentTreeScript = makeComponentTreeScript;
19
+ exports.makeInspectElementScript = makeInspectElementScript;
20
+ /** RN internals + navigation/safe-area wrappers we always strip from the tree. */
21
+ const SKIP_NAMES = [
22
+ 'View',
23
+ 'RCTView',
24
+ 'RCTText',
25
+ 'RCTScrollView',
26
+ 'RCTScrollContentView',
27
+ 'RCTImageView',
28
+ 'RCTSafeAreaView',
29
+ 'RCTVirtualText',
30
+ 'RCTSinglelineTextInputView',
31
+ 'RCTMultilineTextInputView',
32
+ 'RNCSafeAreaProvider',
33
+ 'RNSScreen',
34
+ 'RNSScreenStack',
35
+ 'RNSScreenContentWrapper',
36
+ 'RNSScreenNavigationContainer',
37
+ 'RNSScreenStackHeaderConfig',
38
+ 'ScreenStackHeaderConfig',
39
+ 'NavigationContent',
40
+ 'PreventRemoveProvider',
41
+ 'EnsureSingleNavigator',
42
+ 'StaticContainer',
43
+ 'SceneView',
44
+ 'NativeStackView',
45
+ 'NativeStackNavigator',
46
+ 'DelayedFreeze',
47
+ 'Freeze',
48
+ 'Suspender',
49
+ 'DebugContainer',
50
+ 'ScreenContentWrapper',
51
+ 'Screen',
52
+ 'ScreenStack',
53
+ 'ScreenContainer',
54
+ 'MaybeScreenContainer',
55
+ 'MaybeScreen',
56
+ 'FrameSizeProvider',
57
+ 'FrameSizeProviderInner',
58
+ 'FrameSizeListenerNativeFallback',
59
+ 'SafeAreaProviderCompat',
60
+ 'SafeAreaProvider',
61
+ 'SafeAreaInsetsContext',
62
+ 'SafeArea',
63
+ 'SafeAreaFrameContext',
64
+ 'ErrorOverlay',
65
+ 'ErrorToastContainer',
66
+ 'PerformanceLoggerContext',
67
+ 'AppContainer',
68
+ 'RootTagContext',
69
+ 'DebuggingOverlay',
70
+ 'DebuggingOverlayRegistrySubscription',
71
+ 'LogBoxStateSubscription',
72
+ '_LogBoxNotificationContainer',
73
+ 'LogBoxInspectorContainer',
74
+ 'LogBoxInspector',
75
+ 'LogBoxInspectorCodeFrame',
76
+ 'CellRenderer',
77
+ 'VirtualizedListContextProvider',
78
+ 'VirtualizedListCellContextProvider',
79
+ 'wrapper',
80
+ 'Background',
81
+ 'Pressable',
82
+ 'PlatformPressable',
83
+ 'ExpoRoot',
84
+ 'ContextNavigator',
85
+ 'RootApp',
86
+ 'ThemeProvider',
87
+ 'StatusBar',
88
+ 'ReactNativeProfiler',
89
+ 'NavigationRouteContext',
90
+ 'BottomTabNavigator',
91
+ 'BottomTabView',
92
+ 'ImageAnalyticsTagContext',
93
+ 'GestureHandlerRootView',
94
+ 'GestureDetector',
95
+ 'Wrap',
96
+ 'NavigationContainerInner',
97
+ 'BaseNavigationContainer',
98
+ 'PlatformPressableInternal',
99
+ ];
100
+ const HARD_SKIP_NAMES = [
101
+ 'BaseTextInput',
102
+ 'InternalTextInput',
103
+ 'RNTextInputWithRef',
104
+ 'RCTSinglelineTextInputView',
105
+ 'RCTMultilineTextInputView',
106
+ ];
107
+ /**
108
+ * Component-tree walker. Returns a script that, when evaluated, returns 'ok'
109
+ * synchronously and posts a JSON payload `{ requestId, components, screenW,
110
+ * screenH }` via `__conductor_callback(payload)`.
111
+ *
112
+ * Components carry `{ name, depth, rect, testID, label, text }` for nodes
113
+ * that survive SKIP filtering. `rect` is in window coordinates and is
114
+ * populated via batched `UIManager.measureInWindow` on Paper, or
115
+ * `nativeFabricUIManager.measure` on Fabric.
116
+ */
117
+ function makeComponentTreeScript(requestId) {
118
+ const skip = JSON.stringify(SKIP_NAMES);
119
+ const hardSkip = JSON.stringify(HARD_SKIP_NAMES);
120
+ return `(async function() {
121
+ var REQ = ${JSON.stringify(requestId)};
122
+ function done(payload) {
123
+ try { globalThis.__conductor_callback(JSON.stringify(Object.assign({ requestId: REQ }, payload))); } catch (e) {}
124
+ }
125
+ try {
126
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
127
+ if (!hook) { done({ error: 'No React DevTools hook' }); return 'ok'; }
128
+ var roots = hook.getFiberRoots ? hook.getFiberRoots(1) : null;
129
+ if (!roots || roots.size === 0) { done({ error: 'No fiber roots' }); return 'ok'; }
130
+ var root = Array.from(roots)[0];
131
+
132
+ var useFabric = typeof nativeFabricUIManager !== 'undefined';
133
+ var UIManagerMod = null;
134
+ if (!useFabric) {
135
+ try {
136
+ if (typeof __r === 'function' && typeof __r.getModules === 'function') {
137
+ var mods = __r.getModules();
138
+ for (var e of mods) {
139
+ if (!e[1].isInitialized) continue;
140
+ try {
141
+ var m = __r(e[0]);
142
+ if (m && m.UIManager) { UIManagerMod = m.UIManager; break; }
143
+ } catch (er) {}
144
+ }
145
+ }
146
+ if (!UIManagerMod && typeof __r === 'function') {
147
+ for (var i = 0; i < 300; i++) {
148
+ try {
149
+ var m2 = __r(i);
150
+ if (m2 && m2.UIManager) { UIManagerMod = m2.UIManager; break; }
151
+ } catch (er) {}
152
+ }
153
+ }
154
+ } catch (er) {}
155
+ }
156
+
157
+ var SKIP = new Set(${skip});
158
+ var HARD = new Set(${hardSkip});
159
+ function isHardSkip(n) {
160
+ if (HARD.has(n)) return true;
161
+ if (n.indexOf('AnimatedComponent(') === 0) return true;
162
+ if (n.indexOf('Animated(') === 0) return true;
163
+ return false;
164
+ }
165
+ function shouldSkip(n) {
166
+ if (isHardSkip(n)) return true;
167
+ if (SKIP.has(n)) return true;
168
+ if (n.charAt(0) === '_' && n.charAt(1) === '_') return true;
169
+ if (n.length > 8 && n.slice(-8) === 'Provider') return true;
170
+ if (n.length > 7 && n.slice(-7) === 'Context') return true;
171
+ if (n.indexOf('Route(') === 0) return true;
172
+ return false;
173
+ }
174
+ function getName(f) {
175
+ var t = f.type;
176
+ if (!t) return null;
177
+ if (typeof t === 'string') return t;
178
+ return t.displayName || t.name || null;
179
+ }
180
+ function getProps(f) { return f.memoizedProps || null; }
181
+ function getHostInfo(f) {
182
+ if (typeof f.type !== 'string' || !f.stateNode) return null;
183
+ if (useFabric && f.stateNode.node) return { fabric: true, node: f.stateNode.node };
184
+ if (!useFabric) {
185
+ if (f.stateNode.canonical && typeof f.stateNode.canonical.nativeTag === 'number')
186
+ return { fabric: false, tag: f.stateNode.canonical.nativeTag };
187
+ if (typeof f.stateNode._nativeTag === 'number')
188
+ return { fabric: false, tag: f.stateNode._nativeTag };
189
+ }
190
+ return null;
191
+ }
192
+ function findHost(f, d) {
193
+ if (!f || d > 15) return null;
194
+ var hi = getHostInfo(f);
195
+ if (hi) return hi;
196
+ return findHost(f.child, d + 1);
197
+ }
198
+
199
+ // Screen dimensions via Dimensions API.
200
+ var screenW = 0, screenH = 0;
201
+ try {
202
+ if (typeof __r === 'function' && typeof __r.getModules === 'function') {
203
+ var mods2 = __r.getModules();
204
+ for (var e2 of mods2) {
205
+ if (!e2[1].isInitialized) continue;
206
+ try {
207
+ var mm = __r(e2[0]);
208
+ if (mm && mm.Dimensions && typeof mm.Dimensions.get === 'function') {
209
+ var w = mm.Dimensions.get('window');
210
+ if (w && w.width) { screenW = w.width; screenH = w.height; break; }
211
+ }
212
+ } catch (er) {}
213
+ }
214
+ }
215
+ } catch (er) {}
216
+
217
+ // Walk fibers, collect candidates.
218
+ var candidates = [];
219
+ var stack = [{ f: root.current, d: 0 }];
220
+ while (stack.length && candidates.length < 2000) {
221
+ var item = stack.pop();
222
+ var f = item.f, d = item.d;
223
+ if (!f) continue;
224
+ var name = getName(f);
225
+ if (name && !shouldSkip(name)) {
226
+ var hi = findHost(f, 0);
227
+ var props = getProps(f);
228
+ candidates.push({
229
+ name: name,
230
+ depth: d,
231
+ testID: (props && (props.testID || props['data-testid'])) || null,
232
+ label: (props && (props.accessibilityLabel || props['aria-label'])) || null,
233
+ text: (props && typeof props.children === 'string') ? props.children : null,
234
+ host: hi,
235
+ });
236
+ }
237
+ if (f.sibling) stack.push({ f: f.sibling, d: d });
238
+ if (f.child) stack.push({ f: f.child, d: d + 1 });
239
+ }
240
+
241
+ // Batch measure rects.
242
+ function measureFabric(node) {
243
+ try {
244
+ var r = nativeFabricUIManager.measure(node, function() {});
245
+ if (Array.isArray(r) && r.length >= 6) {
246
+ return { x: r[4], y: r[5], w: r[2], h: r[3] };
247
+ }
248
+ } catch (e) {}
249
+ return null;
250
+ }
251
+ function measurePaper(tag) {
252
+ return new Promise(function(res) {
253
+ try {
254
+ UIManagerMod.measureInWindow(tag, function(x, y, w, h) {
255
+ res({ x: x, y: y, w: w, h: h });
256
+ });
257
+ } catch (e) { res(null); }
258
+ });
259
+ }
260
+
261
+ var promises = [];
262
+ for (var c of candidates) {
263
+ if (!c.host) { promises.push(Promise.resolve(null)); continue; }
264
+ if (c.host.fabric) {
265
+ promises.push(Promise.resolve(measureFabric(c.host.node)));
266
+ } else if (UIManagerMod) {
267
+ promises.push(measurePaper(c.host.tag));
268
+ } else {
269
+ promises.push(Promise.resolve(null));
270
+ }
271
+ }
272
+ var rects = await Promise.all(promises);
273
+ var components = candidates.map(function(c, i) {
274
+ return {
275
+ name: c.name,
276
+ depth: c.depth,
277
+ testID: c.testID,
278
+ label: c.label,
279
+ text: c.text,
280
+ rect: rects[i],
281
+ };
282
+ });
283
+
284
+ done({ screenW: screenW, screenH: screenH, fabric: useFabric, components: components });
285
+ return 'ok';
286
+ } catch (e) {
287
+ done({ error: String((e && e.message) || e) });
288
+ return 'ok';
289
+ }
290
+ })();`;
291
+ }
292
+ /**
293
+ * Inspect-at-point script. Uses React DevTools's own
294
+ * `renderer.rendererConfig.getInspectorDataForViewAtPoint(inspectRef, x, y, cb)`,
295
+ * which is the authoritative point lookup. Then walks UP via `.return` from
296
+ * `data.closestInstance`, preferring `_debugStack` for source resolution and
297
+ * falling back to `_debugSource`.
298
+ */
299
+ function makeInspectElementScript(x, y, requestId) {
300
+ return `(function() {
301
+ var REQ = ${JSON.stringify(requestId)};
302
+ function done(payload) {
303
+ try { globalThis.__conductor_callback(JSON.stringify(Object.assign({ requestId: REQ }, payload))); } catch (e) {}
304
+ }
305
+ try {
306
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
307
+ if (!hook) { done({ error: 'No React DevTools hook' }); return 'ok'; }
308
+ var renderer = Array.from(hook.renderers.values())[0];
309
+ var roots = hook.getFiberRoots(1);
310
+ if (!roots || roots.size === 0) { done({ error: 'No fiber roots' }); return 'ok'; }
311
+ var root = Array.from(roots)[0];
312
+
313
+ var useFabric = typeof nativeFabricUIManager !== 'undefined';
314
+
315
+ function findHostFiber(f, d) {
316
+ if (!f || d > 30) return null;
317
+ if (typeof f.type === 'string' && f.stateNode) {
318
+ if (useFabric && f.stateNode.node) return f;
319
+ if (!useFabric && f.stateNode.canonical) return f;
320
+ }
321
+ return findHostFiber(f.child, d + 1) || null;
322
+ }
323
+
324
+ function getName(f) {
325
+ var t = f.type;
326
+ if (!t || typeof t === 'string') return null;
327
+ if (typeof t === 'function') return t.displayName || t.name || null;
328
+ if (typeof t === 'object') {
329
+ var inner = t.render || t.type;
330
+ if (inner && typeof inner === 'function') return inner.displayName || inner.name || null;
331
+ return t.displayName || null;
332
+ }
333
+ return null;
334
+ }
335
+
336
+ function parseFrame(stack) {
337
+ if (!stack) return null;
338
+ var s = typeof stack === 'string' ? stack : (stack.stack || '');
339
+ var lines = s.split('\\n').slice(1).filter(function(l) { return l.trim().indexOf('at ') === 0; });
340
+ var target = lines[1] || lines[0];
341
+ if (!target) return null;
342
+ var m = target.trim().match(/at (?:([^\\s(]+) \\()?([^)]+):(\\d+):(\\d+)\\)?/);
343
+ return m ? { fn: m[1] || 'anon', file: m[2], line: parseInt(m[3]), col: parseInt(m[4]) } : null;
344
+ }
345
+
346
+ function getFrame(fiber) {
347
+ var frame = parseFrame(fiber._debugStack);
348
+ if (frame) return frame;
349
+ var ds = fiber._debugSource;
350
+ if (ds && ds.fileName) {
351
+ return { fn: 'component', file: ds.fileName, line: ds.lineNumber || 0, col: ds.columnNumber || 0, original: true };
352
+ }
353
+ return null;
354
+ }
355
+
356
+ var hostFiber = findHostFiber(root.current.child, 0);
357
+ if (!hostFiber) { done({ error: 'no host fiber' }); return 'ok'; }
358
+
359
+ var inspectRef;
360
+ if (useFabric) {
361
+ inspectRef = hostFiber.stateNode;
362
+ } else {
363
+ inspectRef = hostFiber.stateNode.canonical && hostFiber.stateNode.canonical.publicInstance;
364
+ }
365
+ if (!inspectRef) { done({ error: 'no inspect ref' }); return 'ok'; }
366
+
367
+ var cfg = renderer.rendererConfig;
368
+ if (!cfg || typeof cfg.getInspectorDataForViewAtPoint !== 'function') {
369
+ done({ error: 'rendererConfig.getInspectorDataForViewAtPoint unavailable' });
370
+ return 'ok';
371
+ }
372
+
373
+ cfg.getInspectorDataForViewAtPoint(inspectRef, ${Math.round(x)}, ${Math.round(y)}, function(data) {
374
+ try {
375
+ var items = [];
376
+ var fiber = data.closestInstance;
377
+ if (fiber) {
378
+ var f = fiber, depth = 0;
379
+ while (f && depth < 200) {
380
+ var nm = getName(f);
381
+ if (nm) items.push({ name: nm, depth: depth, frame: getFrame(f) });
382
+ f = f.return;
383
+ depth++;
384
+ }
385
+ } else if (data.hierarchy && data.hierarchy.length) {
386
+ for (var hi of data.hierarchy) items.push({ name: hi.name, depth: 0, frame: null });
387
+ }
388
+ done({ x: ${Math.round(x)}, y: ${Math.round(y)}, items: items });
389
+ } catch (e) {
390
+ done({ error: String((e && e.message) || e) });
391
+ }
392
+ });
393
+ return 'ok';
394
+ } catch (e) {
395
+ done({ error: String((e && e.message) || e) });
396
+ return 'ok';
397
+ }
398
+ })();`;
399
+ }
@@ -17,11 +17,28 @@ const utils_js_1 = require("../utils.js");
17
17
  const DEFAULT_TIMEOUT_MS = 17000;
18
18
  const DEFAULT_INTERVAL_MS = 500;
19
19
  exports.OPTIONAL_TIMEOUT_MS = 7000;
20
- async function waitForIOSElement(getHierarchy, selector, timeoutMs = DEFAULT_TIMEOUT_MS, intervalMs = DEFAULT_INTERVAL_MS) {
20
+ async function waitForIOSElement(getHierarchy, selector, timeoutMs = DEFAULT_TIMEOUT_MS, intervalMs = DEFAULT_INTERVAL_MS, directResolve) {
21
21
  const deadline = Date.now() + timeoutMs;
22
+ let attempt = 0;
22
23
  while (Date.now() < deadline) {
24
+ // Fast path: resolve a simple selector via a direct runner query,
25
+ // skipping the full-tree snapshot entirely. A `null` result means the
26
+ // element is absent or ambiguous — fall through to the snapshot matcher.
27
+ if (directResolve) {
28
+ try {
29
+ const fast = await directResolve();
30
+ if (fast)
31
+ return fast;
32
+ }
33
+ catch {
34
+ // Direct query failed (transport error / endpoint unavailable) —
35
+ // fall through to the snapshot path.
36
+ }
37
+ }
23
38
  try {
24
- const root = await getHierarchy();
39
+ // Reuse a recently-captured hierarchy only on the first probe; later
40
+ // retries must observe fresh UI so the loop can see the element appear.
41
+ const root = await getHierarchy({ cached: attempt === 0 });
25
42
  const el = (0, element_resolver_js_1.findIOSElement)(root, selector);
26
43
  if (el)
27
44
  return el;
@@ -29,6 +46,7 @@ async function waitForIOSElement(getHierarchy, selector, timeoutMs = DEFAULT_TIM
29
46
  catch {
30
47
  // Hierarchy fetch failed; keep retrying
31
48
  }
49
+ attempt++;
32
50
  await (0, utils_js_1.sleep)(intervalMs);
33
51
  }
34
52
  const desc = selectorDesc(selector);
@@ -121,8 +121,9 @@ class WebDriver {
121
121
  async viewHierarchy() {
122
122
  return this.get('viewHierarchy');
123
123
  }
124
- async screenshot() {
125
- const { status, data } = await this.request('GET', '/screenshot');
124
+ async screenshot(opts = {}) {
125
+ const path = opts.fullPage ? '/screenshot?fullPage=1' : '/screenshot';
126
+ const { status, data } = await this.request('GET', path);
126
127
  if (status < 200 || status >= 300) {
127
128
  throw new Error(`Web driver screenshot failed (HTTP ${status})`);
128
129
  }