react-native-tapline 0.0.0-stage → 1.0.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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/TaplineNative.podspec +14 -0
  4. package/dist/actions.d.ts +160 -0
  5. package/dist/actions.js +1883 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent.d.ts +79 -0
  8. package/dist/agent.js +255 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/alerts.d.ts +35 -0
  11. package/dist/alerts.js +437 -0
  12. package/dist/alerts.js.map +1 -0
  13. package/dist/awake.d.ts +2 -0
  14. package/dist/awake.js +53 -0
  15. package/dist/awake.js.map +1 -0
  16. package/dist/checks.d.ts +19 -0
  17. package/dist/checks.js +318 -0
  18. package/dist/checks.js.map +1 -0
  19. package/dist/client.d.ts +111 -0
  20. package/dist/client.js +446 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/commands.d.ts +24 -0
  23. package/dist/commands.js +111 -0
  24. package/dist/commands.js.map +1 -0
  25. package/dist/events.d.ts +5 -0
  26. package/dist/events.js +22 -0
  27. package/dist/events.js.map +1 -0
  28. package/dist/feed.d.ts +117 -0
  29. package/dist/feed.js +440 -0
  30. package/dist/feed.js.map +1 -0
  31. package/dist/fiber.d.ts +43 -0
  32. package/dist/fiber.js +100 -0
  33. package/dist/fiber.js.map +1 -0
  34. package/dist/index.d.ts +50 -0
  35. package/dist/index.js +133 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/inspect.d.ts +245 -0
  38. package/dist/inspect.js +1343 -0
  39. package/dist/inspect.js.map +1 -0
  40. package/dist/keyboard.d.ts +10 -0
  41. package/dist/keyboard.js +63 -0
  42. package/dist/keyboard.js.map +1 -0
  43. package/dist/link.d.ts +21 -0
  44. package/dist/link.js +63 -0
  45. package/dist/link.js.map +1 -0
  46. package/dist/live.d.ts +30 -0
  47. package/dist/live.js +279 -0
  48. package/dist/live.js.map +1 -0
  49. package/dist/logs.d.ts +16 -0
  50. package/dist/logs.js +171 -0
  51. package/dist/logs.js.map +1 -0
  52. package/dist/methods.d.ts +3 -0
  53. package/dist/methods.js +47 -0
  54. package/dist/methods.js.map +1 -0
  55. package/dist/native.d.ts +34 -0
  56. package/dist/native.js +63 -0
  57. package/dist/native.js.map +1 -0
  58. package/dist/network.d.ts +27 -0
  59. package/dist/network.js +238 -0
  60. package/dist/network.js.map +1 -0
  61. package/dist/register.d.ts +24 -0
  62. package/dist/register.js +180 -0
  63. package/dist/register.js.map +1 -0
  64. package/dist/settle.d.ts +27 -0
  65. package/dist/settle.js +70 -0
  66. package/dist/settle.js.map +1 -0
  67. package/dist/storage.d.ts +10 -0
  68. package/dist/storage.js +45 -0
  69. package/dist/storage.js.map +1 -0
  70. package/dist/touches.d.ts +15 -0
  71. package/dist/touches.js +89 -0
  72. package/dist/touches.js.map +1 -0
  73. package/dist/types.d.ts +168 -0
  74. package/dist/types.js +32 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/wait.d.ts +8 -0
  77. package/dist/wait.js +99 -0
  78. package/dist/wait.js.map +1 -0
  79. package/expo-module.config.json +7 -0
  80. package/ios/TaplineNativeModule.swift +79 -0
  81. package/metro.d.ts +10 -0
  82. package/metro.js +40 -0
  83. package/package.json +100 -4
  84. package/register.d.ts +1 -0
  85. package/register.js +3 -0
@@ -0,0 +1,1343 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultHitTester = exports.defaultMeasurer = exports.MAX_KEPT_TEXT = exports.MAX_TEXT = void 0;
4
+ exports.sliceWhole = sliceWhole;
5
+ exports.cutText = cutText;
6
+ exports.displayText = displayText;
7
+ exports.hidesSubtree = hidesSubtree;
8
+ exports.collectNodes = collectNodes;
9
+ exports.currentRoute = currentRoute;
10
+ exports.findProp = findProp;
11
+ exports.findHandler = findHandler;
12
+ exports.isScrollNode = isScrollNode;
13
+ exports.isInputNode = isInputNode;
14
+ exports.isSwitchNode = isSwitchNode;
15
+ exports.rememberTyped = rememberTyped;
16
+ exports.currentInputValue = currentInputValue;
17
+ exports.roleName = roleName;
18
+ exports.describeNodes = describeNodes;
19
+ exports.publicInstancesOf = publicInstancesOf;
20
+ exports.setMeasurer = setMeasurer;
21
+ exports.measureFiber = measureFiber;
22
+ exports.openModalHosts = openModalHosts;
23
+ exports.closingModalHosts = closingModalHosts;
24
+ exports.modalOpen = modalOpen;
25
+ exports.setHitTester = setHitTester;
26
+ exports.hitTest = hitTest;
27
+ exports.isWithin = isWithin;
28
+ exports.getScreen = getScreen;
29
+ exports.isOnScreen = isOnScreen;
30
+ exports.frameContains = frameContains;
31
+ exports.stableObjectKey = stableObjectKey;
32
+ exports.takeSnapshot = takeSnapshot;
33
+ exports.nativeTabBar = nativeTabBar;
34
+ exports.tabAt = tabAt;
35
+ exports.nativeNavBar = nativeNavBar;
36
+ exports.inNavBarHeader = inNavBarHeader;
37
+ exports.resetRefs = resetRefs;
38
+ exports.refFor = refFor;
39
+ exports.overlay = overlay;
40
+ exports.fiberForRef = fiberForRef;
41
+ exports.toElement = toElement;
42
+ exports.forgetLastTree = forgetLastTree;
43
+ exports.foldDuplicates = foldDuplicates;
44
+ exports.tree = tree;
45
+ const react_native_1 = require("react-native");
46
+ const fiber_1 = require("./fiber");
47
+ const alerts_1 = require("./alerts");
48
+ const keyboard_1 = require("./keyboard");
49
+ const native_1 = require("./native");
50
+ const network_1 = require("./network");
51
+ const register_1 = require("./register");
52
+ const types_1 = require("./types");
53
+ /* -------------------------------------------------------------------------- */
54
+ /* Host-node collection */
55
+ /* -------------------------------------------------------------------------- */
56
+ const TEXT_HOSTS = new Set(['Text', 'RCTText', 'RCTVirtualText', 'AndroidTextView', 'ReactTextView']);
57
+ /** How much of an element's text a line shows (AGENT-TOOLS v1.5); a cut ends with `…(+N)`, the count left out. */
58
+ exports.MAX_TEXT = 200;
59
+ /**
60
+ * How much text a container keeps for matching, and how much of any text goes over the wire. A Text
61
+ * host keeps all of its own text, so `wait_for` and `{text}` targets check all of it.
62
+ */
63
+ exports.MAX_KEPT_TEXT = 2000;
64
+ /** The first `max` UTF-16 units of a text, one fewer when that would cut a character (an emoji) in half. */
65
+ function sliceWhole(text, max) {
66
+ const last = text.charCodeAt(max - 1);
67
+ return text.slice(0, last >= 0xd800 && last <= 0xdbff ? max - 1 : max);
68
+ }
69
+ /** Text as a line quotes it: whitespace flattened, cut at `max` with `…(+N)` (N counts `more` too: what was cut off before). */
70
+ function cutText(raw, more = 0, max = exports.MAX_TEXT) {
71
+ const flat = raw.replace(/\s+/g, ' ').trim();
72
+ const shown = flat.length > max ? sliceWhole(flat, max) : flat;
73
+ const missing = flat.length - shown.length + more;
74
+ return missing > 0 ? `${shown}…(+${missing})` : shown;
75
+ }
76
+ /** Invisible direction marks (AGENT-TOOLS v1.3): U+200E, U+200F, U+202A–U+202E, U+2066–U+2069. */
77
+ const DIRECTION_MARKS = /[‎‏‪-‮⁦-⁩]/g;
78
+ /** Text as an agent sees and matches it: without the invisible direction marks. */
79
+ function displayText(raw) {
80
+ return raw.replace(DIRECTION_MARKS, '');
81
+ }
82
+ function normalizeText(raw, whole) {
83
+ const collapsed = displayText(raw).replace(/\s+/g, ' ').trim();
84
+ return !whole && collapsed.length > exports.MAX_KEPT_TEXT ? collapsed.slice(0, exports.MAX_KEPT_TEXT) : collapsed;
85
+ }
86
+ /** React Native Screens' tab screens (native tabs: iOS, Android, and the older bottom-tabs name). */
87
+ const TAB_SCREEN = /^RNS(Tabs|BottomTabs)Screen/;
88
+ function displaysNone(style) {
89
+ try {
90
+ const flat = react_native_1.StyleSheet.flatten(style);
91
+ return (0, types_1.isRecord)(flat) && flat.display === 'none';
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * Whether a host element and everything inside it is out of the user's reach, so Tapline leaves
99
+ * it out: hidden from accessibility (React Native Screens marks a screen covered by another this
100
+ * way, React Navigation its unfocused screens), not displayed, an inactive screen, or a tab that
101
+ * isn't selected (native tabs keep every tab mounted and turn off touches on the hidden ones).
102
+ */
103
+ function hidesSubtree(hostType, props) {
104
+ if (props['aria-hidden'] === true)
105
+ return true;
106
+ if (props.accessibilityElementsHidden === true)
107
+ return true;
108
+ if (props.importantForAccessibility === 'no-hide-descendants')
109
+ return true;
110
+ if (props.style !== undefined && displaysNone(props.style))
111
+ return true;
112
+ if (hostType === 'RNSScreen' && props.activityState === 0)
113
+ return true;
114
+ if (TAB_SCREEN.test(hostType) && (props.pointerEvents === 'none' || props.isFocused === false))
115
+ return true;
116
+ return false;
117
+ }
118
+ /** React Native Screens' native tab containers. */
119
+ const TAB_HOST = /^RNS(Tabs|BottomTabs)Host/;
120
+ function routeOf(fiber, chain) {
121
+ const props = (0, fiber_1.propsOf)(fiber);
122
+ const route = props.route;
123
+ const nav = props.navigation;
124
+ if (!(0, types_1.isRecord)(route) || !(0, types_1.isRecord)(nav))
125
+ return null;
126
+ if (typeof route.key !== 'string' || typeof route.name !== 'string' || typeof nav.goBack !== 'function')
127
+ return null;
128
+ if (chain.length > 0 && chain[chain.length - 1]?.key === route.key)
129
+ return null; // the same screen, passed on
130
+ const navigation = nav;
131
+ let focused = true;
132
+ try {
133
+ // The navigator's state decides when it lists this route: the route is focused when it is the
134
+ // current one. isFocused() alone isn't enough: a JavaScript tab bar passes every tab's route
135
+ // to its buttons with the navigator's own navigation, which is focused for all of them.
136
+ const state = typeof navigation.getState === 'function' ? navigation.getState() : undefined;
137
+ const routes = (0, types_1.isRecord)(state) && Array.isArray(state.routes) ? state.routes : [];
138
+ const at = routes.findIndex((r) => (0, types_1.isRecord)(r) && r.key === route.key);
139
+ if (at >= 0 && (0, types_1.isRecord)(state) && typeof state.index === 'number')
140
+ focused = at === state.index;
141
+ else if (typeof navigation.isFocused === 'function')
142
+ focused = navigation.isFocused() !== false;
143
+ }
144
+ catch {
145
+ focused = true;
146
+ }
147
+ return {
148
+ key: route.key,
149
+ name: route.name,
150
+ params: route.params,
151
+ navigation,
152
+ focused,
153
+ chain: [...chain.map((r) => r.name), route.name],
154
+ };
155
+ }
156
+ /** The tab screens under a native tab container, hidden ones included. */
157
+ function collectTabs(first, tabs) {
158
+ for (let fiber = first; fiber; fiber = fiber.sibling) {
159
+ const hostType = fiber.tag === fiber_1.HostComponent && typeof fiber.type === 'string' ? fiber.type : '';
160
+ if (TAB_SCREEN.test(hostType)) {
161
+ const props = (0, fiber_1.propsOf)(fiber);
162
+ const screenKey = typeof props.screenKey === 'string' ? props.screenKey : null;
163
+ const fromKey = screenKey !== null && screenKey.includes('-') ? screenKey.slice(0, screenKey.lastIndexOf('-')) : screenKey;
164
+ const title = typeof props.title === 'string' && props.title !== '' ? props.title : (fromKey ?? 'Tab');
165
+ tabs.push({ fiber, title, screenKey, selected: !hidesSubtree(hostType, props) });
166
+ }
167
+ else {
168
+ collectTabs(fiber.child, tabs);
169
+ }
170
+ }
171
+ }
172
+ function walk(first, parent, owners, insideText, out, chain) {
173
+ const segments = [];
174
+ for (let fiber = first; fiber; fiber = fiber.sibling) {
175
+ if (fiber.tag === fiber_1.HostText) {
176
+ const value = fiber.memoizedProps;
177
+ if (typeof value === 'string' && value !== '')
178
+ segments.push(value);
179
+ }
180
+ else if (fiber.tag === fiber_1.HostComponent) {
181
+ const hostType = typeof fiber.type === 'string' ? fiber.type : '';
182
+ if (TAB_HOST.test(hostType))
183
+ collectTabs(fiber.child, out.tabs);
184
+ if (hidesSubtree(hostType, (0, fiber_1.propsOf)(fiber)))
185
+ continue;
186
+ if (MODAL_HOSTS.has(hostType))
187
+ out.modals.push(fiber);
188
+ const isText = TEXT_HOSTS.has(hostType);
189
+ const node = {
190
+ index: out.nodes.length,
191
+ parent,
192
+ fiber,
193
+ hostType,
194
+ owners: owners.slice().reverse(),
195
+ text: '',
196
+ isText,
197
+ insideText,
198
+ };
199
+ out.nodes.push(node);
200
+ const inner = walk(fiber.child, node.index, [], insideText || isText, out, chain);
201
+ node.text = normalizeText(isText ? inner.join('') : inner.join(' '), isText);
202
+ if (isText)
203
+ segments.push(inner.join(''));
204
+ else
205
+ for (const piece of inner)
206
+ segments.push(piece);
207
+ }
208
+ else if ((0, fiber_1.isCompositeTag)(fiber.tag)) {
209
+ const route = routeOf(fiber, chain);
210
+ if (route)
211
+ out.routes.push(route);
212
+ const inner = walk(fiber.child, parent, [...owners, fiber], insideText, out, route ? [...chain, route] : chain);
213
+ for (const piece of inner)
214
+ segments.push(piece);
215
+ }
216
+ else {
217
+ const inner = walk(fiber.child, parent, owners, insideText, out, chain);
218
+ for (const piece of inner)
219
+ segments.push(piece);
220
+ }
221
+ }
222
+ return segments;
223
+ }
224
+ function collect(roots) {
225
+ const out = { nodes: [], routes: [], tabs: [], modals: [] };
226
+ for (const root of roots)
227
+ walk(root.current.child, -1, [], false, out, []);
228
+ // iOS hides what is behind a modal from accessibility, so a person (and Tapwright, which reads
229
+ // that tree) sees only the topmost modal: list only that, no screen behind it and no tab bar.
230
+ const top = out.modals[out.modals.length - 1];
231
+ if (!top)
232
+ return out;
233
+ const only = { nodes: [], routes: [], tabs: [], modals: [] };
234
+ walk(top.child, -1, [], false, only, []);
235
+ return { nodes: only.nodes, routes: out.routes, tabs: [], modals: out.modals, behind: { sheet: top, nodes: out.nodes } };
236
+ }
237
+ function collectNodes(roots = (0, register_1.getRoots)()) {
238
+ return collect(roots).nodes;
239
+ }
240
+ /** The focused screen: the deepest focused route, the last one on ties (drawn on top). */
241
+ function currentRoute(routes) {
242
+ let best = null;
243
+ for (const route of routes) {
244
+ if (!route.focused)
245
+ continue;
246
+ if (best === null || route.chain.length >= best.chain.length)
247
+ best = route;
248
+ }
249
+ return best;
250
+ }
251
+ /** Route params that read well in a line: top-level strings (cut to 80), numbers, booleans, null. */
252
+ function simpleParams(params) {
253
+ if (!(0, types_1.isRecord)(params))
254
+ return undefined;
255
+ const out = {};
256
+ for (const [key, value] of Object.entries(params)) {
257
+ if (typeof value === 'string')
258
+ out[key] = value.length > 80 ? `${value.slice(0, 80)}…` : value;
259
+ else if (typeof value === 'number' || typeof value === 'boolean' || value === null)
260
+ out[key] = value;
261
+ }
262
+ return Object.keys(out).length > 0 ? out : undefined;
263
+ }
264
+ /* -------------------------------------------------------------------------- */
265
+ /* Props lookup and element description */
266
+ /* -------------------------------------------------------------------------- */
267
+ /** First defined prop `key` on the host fiber, then on its owners (innermost first). */
268
+ function findProp(node, key) {
269
+ const own = (0, fiber_1.propsOf)(node.fiber)[key];
270
+ if (own !== undefined)
271
+ return own;
272
+ for (const owner of node.owners) {
273
+ const value = (0, fiber_1.propsOf)(owner)[key];
274
+ if (value !== undefined)
275
+ return value;
276
+ }
277
+ return undefined;
278
+ }
279
+ function findHandler(node, key) {
280
+ const value = findProp(node, key);
281
+ return typeof value === 'function' ? value : null;
282
+ }
283
+ function stateField(node, field) {
284
+ const state = findProp(node, 'accessibilityState');
285
+ return (0, types_1.isRecord)(state) ? state[field] : undefined;
286
+ }
287
+ const PRIMARY_TYPES = new Set([
288
+ 'Pressable',
289
+ 'Button',
290
+ 'TouchableOpacity',
291
+ 'TouchableHighlight',
292
+ 'TouchableWithoutFeedback',
293
+ 'TouchableNativeFeedback',
294
+ 'Switch',
295
+ 'TextInput',
296
+ 'ScrollView',
297
+ 'FlatList',
298
+ 'SectionList',
299
+ 'VirtualizedList',
300
+ 'Text',
301
+ 'Image',
302
+ 'ImageBackground',
303
+ 'Modal',
304
+ 'ActivityIndicator',
305
+ 'SafeAreaView',
306
+ 'KeyboardAvoidingView',
307
+ 'RefreshControl',
308
+ ]);
309
+ function isPlumbing(name) {
310
+ return (PRIMARY_TYPES.has(name) ||
311
+ name === 'View' ||
312
+ name.startsWith('Animated') ||
313
+ name.startsWith('Virtualized') ||
314
+ name.endsWith('CellRenderer') ||
315
+ name.startsWith('ScrollView') ||
316
+ name.startsWith('InternalTextInput') ||
317
+ name.startsWith('RCT') ||
318
+ name.startsWith('Android') ||
319
+ name.startsWith('Context') ||
320
+ name === 'TextAncestor');
321
+ }
322
+ function ownerNames(node) {
323
+ const names = [];
324
+ for (const owner of node.owners) {
325
+ const name = (0, fiber_1.fiberName)(owner);
326
+ if (name)
327
+ names.push(name);
328
+ }
329
+ return names;
330
+ }
331
+ /**
332
+ * Nearest named component that owns the element. React Native's own primitives
333
+ * wrap each other (Pressable -> View), so within the leading run of RN
334
+ * plumbing we report the outermost primary primitive (Pressable, Button, ...).
335
+ */
336
+ function typeName(node) {
337
+ let primary = null;
338
+ let firstNamed = null;
339
+ for (const owner of node.owners) {
340
+ const name = (0, fiber_1.fiberName)(owner);
341
+ if (!name)
342
+ continue;
343
+ if (firstNamed === null)
344
+ firstNamed = name;
345
+ if (!isPlumbing(name))
346
+ break;
347
+ if (PRIMARY_TYPES.has(name))
348
+ primary = name;
349
+ }
350
+ return primary ?? firstNamed ?? (node.isText ? 'Text' : node.hostType || 'View');
351
+ }
352
+ const SCROLL_NAMES = new Set(['ScrollView', 'FlatList', 'SectionList', 'VirtualizedList']);
353
+ function isScrollNode(node) {
354
+ return /ScrollView/.test(node.hostType) || ownerNames(node).some((name) => SCROLL_NAMES.has(name));
355
+ }
356
+ function isInputNode(node) {
357
+ return /TextInput/.test(node.hostType) || ownerNames(node).includes('TextInput');
358
+ }
359
+ function isSwitchNode(node) {
360
+ return /Switch/.test(node.hostType) || ownerNames(node).includes('Switch');
361
+ }
362
+ /** Last text set through Tapline for uncontrolled inputs, keyed by the host stateNode. */
363
+ const typedValues = new WeakMap();
364
+ function rememberTyped(node, text) {
365
+ const key = node.fiber.stateNode;
366
+ if (typeof key === 'object' && key !== null)
367
+ typedValues.set(key, text);
368
+ }
369
+ function currentInputValue(node) {
370
+ const controlled = findProp(node, 'value');
371
+ if (typeof controlled === 'string')
372
+ return controlled;
373
+ const key = node.fiber.stateNode;
374
+ if (typeof key === 'object' && key !== null) {
375
+ const typed = typedValues.get(key);
376
+ if (typed !== undefined)
377
+ return typed;
378
+ }
379
+ const initial = findProp(node, 'defaultValue');
380
+ return typeof initial === 'string' ? initial : '';
381
+ }
382
+ /**
383
+ * The shared role vocabulary (AGENT-TOOLS §3.3). A copy of `roleOf` in packages/host/src/format.ts:
384
+ * keep the two tables in step; the packages must not import each other.
385
+ */
386
+ const ROLE_ALIASES = new Map([
387
+ ['textfield', 'input'],
388
+ ['search', 'input'],
389
+ ['textbox', 'input'],
390
+ ['scrollview', 'scroll'],
391
+ ['imagebutton', 'button'],
392
+ ['togglebutton', 'button'],
393
+ ['adjustable', 'slider'],
394
+ ['img', 'image'],
395
+ ['summary', 'text'],
396
+ ]);
397
+ /** Component types that read as a shared role when the app gives none (copy of `TYPE_ROLES` in the host's format.ts). */
398
+ const TYPE_ROLES = new Map([
399
+ ['Image', 'image'],
400
+ ['TextInput', 'input'],
401
+ ['Switch', 'switch'],
402
+ ['Slider', 'slider'],
403
+ ['RNCSlider', 'slider'],
404
+ ['Text', 'text'],
405
+ ['ScrollView', 'scroll'],
406
+ ['FlatList', 'scroll'],
407
+ ['SectionList', 'scroll'],
408
+ ['VirtualizedList', 'scroll'],
409
+ ]);
410
+ function roleName(info) {
411
+ if (info.role !== null && info.role !== '') {
412
+ const lower = info.role.toLowerCase();
413
+ return ROLE_ALIASES.get(lower) ?? lower;
414
+ }
415
+ const byType = TYPE_ROLES.get(info.type);
416
+ if (byType !== undefined)
417
+ return byType;
418
+ const pressable = info.type === 'Pressable' || info.type === 'Button' || info.type.startsWith('Touchable');
419
+ return pressable && info.actions.includes('press') ? 'button' : 'other';
420
+ }
421
+ function stringProp(node, key) {
422
+ const value = findProp(node, key);
423
+ return typeof value === 'string' && value !== '' ? value : null;
424
+ }
425
+ /**
426
+ * Handlers shared with a descendant host belong to the descendant, not to this wrapper. And a
427
+ * testID a wrapper only inherits (from the component that rendered it, through `findProp`'s owners)
428
+ * belongs to a descendant host that sets the same testID itself: a field component that passes its
429
+ * testID on to its TextInput would otherwise name two elements. A host that sets its testID itself
430
+ * (a button) keeps it.
431
+ */
432
+ /** A host that takes a press, long press or toggle (its own or its component's handler), or is an input. */
433
+ function actsItself(node) {
434
+ return (isInputNode(node) ||
435
+ ['onPress', 'onLongPress', 'onValueChange'].some((key) => typeof findProp(node, key) === 'function'));
436
+ }
437
+ function suppressedHandlers(nodes) {
438
+ const suppressed = new Set();
439
+ for (const node of nodes) {
440
+ for (const key of ['onPress', 'onLongPress', 'onValueChange']) {
441
+ const handler = findProp(node, key);
442
+ if (typeof handler !== 'function')
443
+ continue;
444
+ for (let p = node.parent; p >= 0;) {
445
+ const ancestor = nodes[p];
446
+ if (!ancestor)
447
+ break;
448
+ if (findProp(ancestor, key) === handler)
449
+ suppressed.add(`${ancestor.index}:${key}`);
450
+ p = ancestor.parent;
451
+ }
452
+ }
453
+ const own = (0, fiber_1.propsOf)(node.fiber).testID;
454
+ if (typeof own === 'string' && own !== '') {
455
+ // a wrapper above that only inherits this host's testID and takes no action of its own (a
456
+ // field's label-and-input box); a wrapper that acts (a tab item) keeps it, as it's what lists
457
+ for (let p = node.parent; p >= 0;) {
458
+ const ancestor = nodes[p];
459
+ if (!ancestor)
460
+ break;
461
+ if ((0, fiber_1.propsOf)(ancestor.fiber).testID === undefined && findProp(ancestor, 'testID') === own && !actsItself(ancestor)) {
462
+ suppressed.add(`${ancestor.index}:testID`);
463
+ }
464
+ p = ancestor.parent;
465
+ }
466
+ continue;
467
+ }
468
+ // this host only inherits a testID that a host above sets itself (a button's label inside it)
469
+ const inherited = findProp(node, 'testID');
470
+ if (typeof inherited !== 'string' || inherited === '')
471
+ continue;
472
+ for (let p = node.parent; p >= 0;) {
473
+ const ancestor = nodes[p];
474
+ if (!ancestor)
475
+ break;
476
+ if ((0, fiber_1.propsOf)(ancestor.fiber).testID === inherited) {
477
+ suppressed.add(`${node.index}:testID`);
478
+ break;
479
+ }
480
+ p = ancestor.parent;
481
+ }
482
+ }
483
+ return suppressed;
484
+ }
485
+ function describeNodes(nodes) {
486
+ const suppressed = suppressedHandlers(nodes);
487
+ const infos = [];
488
+ for (const node of nodes) {
489
+ const input = isInputNode(node);
490
+ const toggleable = isSwitchNode(node);
491
+ const actions = [];
492
+ const has = (key) => typeof findProp(node, key) === 'function' && !suppressed.has(`${node.index}:${key}`);
493
+ // An input always takes a tap (focus) and then typing (AGENT-TOOLS v1.3): `tap, type`.
494
+ if (has('onPress') || input)
495
+ actions.push('press');
496
+ if (has('onLongPress'))
497
+ actions.push('longPress');
498
+ if (input)
499
+ actions.push('type', 'submit');
500
+ if (toggleable && has('onValueChange'))
501
+ actions.push('toggle');
502
+ if (isScrollNode(node))
503
+ actions.push('scroll');
504
+ const testID = suppressed.has(`${node.index}:testID`) ? null : stringProp(node, 'testID');
505
+ const rawLabel = stringProp(node, 'accessibilityLabel') ?? stringProp(node, 'aria-label');
506
+ const strippedLabel = rawLabel === null ? null : displayText(rawLabel);
507
+ const label = strippedLabel === '' ? null : strippedLabel;
508
+ const disabled = findProp(node, 'disabled') === true ||
509
+ stateField(node, 'disabled') === true ||
510
+ findProp(node, 'aria-disabled') === true ||
511
+ (input && findProp(node, 'editable') === false);
512
+ const selected = findProp(node, 'selected') === true ||
513
+ stateField(node, 'selected') === true ||
514
+ findProp(node, 'aria-selected') === true;
515
+ let checked = null;
516
+ const stateChecked = stateField(node, 'checked');
517
+ const ariaChecked = findProp(node, 'aria-checked');
518
+ if (typeof stateChecked === 'boolean' || stateChecked === 'mixed')
519
+ checked = stateChecked;
520
+ else if (typeof ariaChecked === 'boolean' || ariaChecked === 'mixed')
521
+ checked = ariaChecked;
522
+ else if (toggleable && typeof findProp(node, 'value') === 'boolean') {
523
+ checked = findProp(node, 'value') === true;
524
+ }
525
+ const explicitRole = stringProp(node, 'role') ?? stringProp(node, 'accessibilityRole');
526
+ let role = explicitRole;
527
+ if (role === null) {
528
+ if (toggleable)
529
+ role = 'switch';
530
+ else if (input)
531
+ role = 'textfield';
532
+ else if (actions.includes('press'))
533
+ role = 'button';
534
+ else if (isScrollNode(node))
535
+ role = 'scrollview';
536
+ else if (node.isText)
537
+ role = 'text';
538
+ }
539
+ let redundantText = false;
540
+ if (node.isText && actions.length === 0 && testID === null && label === null) {
541
+ for (let p = node.parent; p >= 0;) {
542
+ const ancestor = nodes[p];
543
+ const info = infos[p];
544
+ if (!ancestor || !info)
545
+ break;
546
+ if (info.actions.length > 0 && info.text === node.text) {
547
+ redundantText = true;
548
+ break;
549
+ }
550
+ p = ancestor.parent;
551
+ }
552
+ }
553
+ // Own text only (AGENT-TOOLS §3.3): an accessible element (what VoiceOver reads as one item)
554
+ // carries the text inside it, the way iOS forms its label; a plain container quotes nothing,
555
+ // so it never matches a text target through its children's text.
556
+ const accessible = node.isText || input || toggleable || actions.includes('press') || actions.includes('longPress') || findProp(node, 'accessible') === true;
557
+ const hasContent = node.isText && !node.insideText && node.text !== '';
558
+ const inputValue = input ? currentInputValue(node) : null;
559
+ infos.push({
560
+ type: typeName(node),
561
+ role,
562
+ testID,
563
+ label,
564
+ text: input || !accessible ? '' : node.text,
565
+ value: inputValue,
566
+ placeholder: inputValue === '' ? stringProp(node, 'placeholder') : null,
567
+ actions,
568
+ disabled,
569
+ selected,
570
+ checked,
571
+ interesting: !redundantText && (actions.length > 0 || testID !== null || label !== null || hasContent),
572
+ });
573
+ }
574
+ return infos;
575
+ }
576
+ const MEASURE_TIMEOUT_MS = 500;
577
+ function isMeasurable(value) {
578
+ return (0, types_1.isRecord)(value) && typeof value.measureInWindow === 'function';
579
+ }
580
+ /**
581
+ * Public instances of a host fiber, in preference order:
582
+ * - old architecture: `stateNode` itself
583
+ * - Fabric: `stateNode.canonical.publicInstance` (null until a ref was requested)
584
+ */
585
+ function publicInstancesOf(fiber) {
586
+ const found = [];
587
+ const state = fiber.stateNode;
588
+ if (!(0, types_1.isRecord)(state))
589
+ return found;
590
+ found.push(state);
591
+ const canonical = state.canonical;
592
+ if ((0, types_1.isRecord)(canonical)) {
593
+ found.push(canonical);
594
+ if (canonical.publicInstance != null)
595
+ found.push(canonical.publicInstance);
596
+ }
597
+ if (state.publicInstance != null)
598
+ found.push(state.publicInstance);
599
+ return found;
600
+ }
601
+ function fabricUIManager() {
602
+ const manager = globalThis.nativeFabricUIManager;
603
+ return (0, types_1.isRecord)(manager) && typeof manager.measureInWindow === 'function'
604
+ ? manager
605
+ : null;
606
+ }
607
+ const defaultMeasurer = (fiber) => new Promise((resolve) => {
608
+ let settled = false;
609
+ const finish = (frame) => {
610
+ if (settled)
611
+ return;
612
+ settled = true;
613
+ clearTimeout(timer);
614
+ resolve(frame);
615
+ };
616
+ const timer = setTimeout(() => finish(null), MEASURE_TIMEOUT_MS);
617
+ const callback = (x, y, width, height) => {
618
+ if ([x, y, width, height].every((n) => typeof n === 'number' && Number.isFinite(n))) {
619
+ finish({ x, y, width, height });
620
+ }
621
+ else
622
+ finish(null);
623
+ };
624
+ try {
625
+ const target = publicInstancesOf(fiber).find(isMeasurable);
626
+ if (target) {
627
+ target.measureInWindow(callback);
628
+ return;
629
+ }
630
+ // Fabric host without a public instance: ask the UIManager about the shadow node.
631
+ const state = fiber.stateNode;
632
+ const manager = fabricUIManager();
633
+ if (manager && (0, types_1.isRecord)(state) && state.node != null) {
634
+ manager.measureInWindow(state.node, callback);
635
+ return;
636
+ }
637
+ finish(null);
638
+ }
639
+ catch {
640
+ finish(null);
641
+ }
642
+ });
643
+ exports.defaultMeasurer = defaultMeasurer;
644
+ let measurer = exports.defaultMeasurer;
645
+ /** Inject a measuring function (tests); pass null to restore the default. */
646
+ function setMeasurer(next) {
647
+ measurer = next ?? exports.defaultMeasurer;
648
+ }
649
+ function measureFiber(fiber) {
650
+ return measurer(fiber).catch(() => null);
651
+ }
652
+ const HIT_TIMEOUT_MS = 300;
653
+ function fabricHitManager() {
654
+ const manager = globalThis.nativeFabricUIManager;
655
+ return (0, types_1.isRecord)(manager) && typeof manager.findNodeAtPoint === 'function'
656
+ ? manager
657
+ : null;
658
+ }
659
+ /** The shadow node of a root's outermost host component (Fabric). */
660
+ function rootShadowNode(root) {
661
+ for (let fiber = root.current.child; fiber; fiber = fiber.child) {
662
+ if (fiber.tag === fiber_1.HostComponent) {
663
+ const state = fiber.stateNode;
664
+ return (0, types_1.isRecord)(state) && state.node != null ? state.node : undefined;
665
+ }
666
+ }
667
+ return undefined;
668
+ }
669
+ function isFiberLike(value) {
670
+ return (0, types_1.isRecord)(value) && typeof value.tag === 'number' && 'return' in value;
671
+ }
672
+ /** React Native's `Modal` host (its Fabric name, and the name the JS side renders). */
673
+ const MODAL_HOSTS = new Set(['RCTModalHostView', 'ModalHostView']);
674
+ function shadowNodeOf(fiber) {
675
+ const state = fiber.stateNode;
676
+ return (0, types_1.isRecord)(state) && state.node != null ? state.node : undefined;
677
+ }
678
+ /** The first host fiber among `first`, its siblings and their descendants (pre-order). */
679
+ function firstHostFiber(first) {
680
+ const stack = first ? [first] : [];
681
+ for (let next = stack.pop(); next; next = stack.pop()) {
682
+ if (next.tag === fiber_1.HostComponent)
683
+ return next;
684
+ if (next.sibling)
685
+ stack.push(next.sibling);
686
+ if (next.child)
687
+ stack.push(next.child);
688
+ }
689
+ return null;
690
+ }
691
+ /**
692
+ * The host fibers of mounted `Modal`s, in tree order. A closed `Modal` renders nothing, but one
693
+ * that is being dismissed stays mounted (with `visible` false) until iOS ends the animation; it
694
+ * still covers the screen, so it is listed here too (see `closingModalHosts`).
695
+ */
696
+ function openModalHosts() {
697
+ const hosts = [];
698
+ for (const root of (0, register_1.getRoots)()) {
699
+ const stack = root.current.child ? [root.current.child] : [];
700
+ for (let fiber = stack.pop(); fiber; fiber = stack.pop()) {
701
+ if (fiber.tag === fiber_1.HostComponent && typeof fiber.type === 'string' && MODAL_HOSTS.has(fiber.type))
702
+ hosts.push(fiber);
703
+ if (fiber.sibling)
704
+ stack.push(fiber.sibling);
705
+ if (fiber.child)
706
+ stack.push(fiber.child);
707
+ }
708
+ }
709
+ return hosts;
710
+ }
711
+ /** The mounted modal hosts that are on their way out: `visible` is false while the dismiss animation runs. */
712
+ function closingModalHosts() {
713
+ return openModalHosts().filter((host) => (0, types_1.isRecord)(host.memoizedProps) && host.memoizedProps.visible === false);
714
+ }
715
+ /** Whether a `Modal` is open: its window covers the whole app, the native tab bar included. */
716
+ function modalOpen() {
717
+ return openModalHosts().length > 0;
718
+ }
719
+ /**
720
+ * The shadow nodes of open modals' content, the last presented first. A `Modal` is its own native
721
+ * window over the app, but its content sits in the same React tree, under a host whose layout
722
+ * the root's hit test never reaches: hit-testing from the root finds the screen behind it.
723
+ */
724
+ function openModalContentNodes() {
725
+ const nodes = [];
726
+ for (const host of openModalHosts()) {
727
+ const content = firstHostFiber(host.child);
728
+ const node = content ? shadowNodeOf(content) : undefined;
729
+ if (node !== undefined)
730
+ nodes.push(node);
731
+ }
732
+ return nodes.reverse();
733
+ }
734
+ /**
735
+ * Fabric's own hit test (`findNodeAtPoint`, as React Native's element inspector uses): open modals
736
+ * first (a modal's window covers the whole screen, so a touch never reaches what's behind it),
737
+ * then the roots, topmost first.
738
+ */
739
+ const defaultHitTester = async (x, y) => {
740
+ const manager = fabricHitManager();
741
+ if (!manager)
742
+ return undefined;
743
+ const roots = (0, register_1.getRoots)().map(rootShadowNode).filter((node) => node !== undefined).reverse();
744
+ const nodes = [...openModalContentNodes(), ...roots];
745
+ if (nodes.length === 0)
746
+ return undefined;
747
+ for (const node of nodes) {
748
+ const hit = await new Promise((resolve) => {
749
+ const timer = setTimeout(() => resolve(undefined), HIT_TIMEOUT_MS);
750
+ try {
751
+ manager.findNodeAtPoint(node, x, y, (handle) => {
752
+ clearTimeout(timer);
753
+ resolve(isFiberLike(handle) ? handle : null);
754
+ });
755
+ }
756
+ catch {
757
+ clearTimeout(timer);
758
+ resolve(undefined);
759
+ }
760
+ });
761
+ if (hit !== null)
762
+ return hit;
763
+ }
764
+ return null;
765
+ };
766
+ exports.defaultHitTester = defaultHitTester;
767
+ let hitTester = exports.defaultHitTester;
768
+ /** Inject a hit tester (tests); pass null to restore the default. */
769
+ function setHitTester(next) {
770
+ hitTester = next ?? exports.defaultHitTester;
771
+ }
772
+ function hitTest(x, y) {
773
+ return hitTester(x, y).catch(() => undefined);
774
+ }
775
+ /** Whether `inner` is `outer` or inside it (following `return` links). */
776
+ function isWithin(inner, outer) {
777
+ for (let fiber = inner; fiber; fiber = fiber.return) {
778
+ if ((0, fiber_1.sameFiber)(fiber, outer))
779
+ return true;
780
+ }
781
+ return false;
782
+ }
783
+ function getScreen() {
784
+ const { width, height } = react_native_1.Dimensions.get('window');
785
+ return { width, height, scale: react_native_1.PixelRatio.get() };
786
+ }
787
+ function isOnScreen(frame, screen) {
788
+ return (frame.width > 0 &&
789
+ frame.height > 0 &&
790
+ frame.x < screen.width &&
791
+ frame.y < screen.height &&
792
+ frame.x + frame.width > 0 &&
793
+ frame.y + frame.height > 0);
794
+ }
795
+ function frameContains(frame, x, y) {
796
+ return x >= frame.x && x <= frame.x + frame.width && y >= frame.y && y <= frame.y + frame.height;
797
+ }
798
+ /**
799
+ * A stand-in fiber per native tab, stable across snapshots so its ref keeps working; the real
800
+ * tab screen fiber is its `return`. (The selected tab's real fiber is also a regular node.)
801
+ */
802
+ const tabFibers = new WeakMap();
803
+ /**
804
+ * An object that stays the same for a host element across re-renders: Fabric clones `stateNode`
805
+ * on every update but keeps its `canonical`; elsewhere the stateNode, else the fiber itself.
806
+ */
807
+ function stableObjectKey(fiber) {
808
+ const state = fiber.stateNode;
809
+ if ((0, types_1.isRecord)(state))
810
+ return (0, types_1.isRecord)(state.canonical) ? state.canonical : state;
811
+ return fiber;
812
+ }
813
+ function tabFiberFor(real) {
814
+ const key = stableObjectKey(real);
815
+ const fake = tabFibers.get(key) ?? {
816
+ tag: -1,
817
+ type: 'Tab',
818
+ elementType: 'Tab',
819
+ memoizedProps: {},
820
+ stateNode: null,
821
+ child: null,
822
+ sibling: null,
823
+ return: null,
824
+ alternate: null,
825
+ };
826
+ tabFibers.set(key, fake);
827
+ fake.return = real;
828
+ return fake;
829
+ }
830
+ /** Take a fresh view of the current fiber tree (no measuring). */
831
+ function takeSnapshot() {
832
+ const roots = (0, register_1.getRoots)();
833
+ if (roots.length === 0) {
834
+ throw new types_1.TaplineError('internal', "No React roots recorded. Import 'react-native-tapline/register' first in the app's entry file, before React.");
835
+ }
836
+ const { nodes, routes, tabs, behind } = collect(roots);
837
+ const infos = describeNodes(nodes);
838
+ const entries = [];
839
+ nodes.forEach((node, i) => {
840
+ const info = infos[i];
841
+ if (info)
842
+ entries.push({ node, info });
843
+ });
844
+ for (const tab of tabs) {
845
+ const node = {
846
+ index: nodes.length,
847
+ parent: -1,
848
+ fiber: tabFiberFor(tab.fiber),
849
+ hostType: 'Tab',
850
+ owners: [],
851
+ text: tab.title,
852
+ isText: false,
853
+ insideText: false,
854
+ };
855
+ nodes.push(node);
856
+ entries.push({
857
+ node,
858
+ tab,
859
+ info: {
860
+ type: 'Tab',
861
+ role: 'tab',
862
+ testID: null,
863
+ label: null,
864
+ text: tab.title,
865
+ value: null,
866
+ placeholder: null,
867
+ actions: ['press'],
868
+ disabled: false,
869
+ selected: tab.selected,
870
+ checked: null,
871
+ interesting: true,
872
+ },
873
+ });
874
+ }
875
+ const snapshot = { nodes, entries, screen: getScreen(), routes };
876
+ if (behind) {
877
+ let worked;
878
+ snapshot.behind = () => {
879
+ if (worked)
880
+ return worked;
881
+ const all = describeNodes(behind.nodes);
882
+ const hidden = [];
883
+ const everything = [];
884
+ behind.nodes.forEach((node, i) => {
885
+ const info = all[i];
886
+ if (!info)
887
+ return;
888
+ const entry = { node, info };
889
+ everything.push(entry);
890
+ if (!isWithin(node.fiber, behind.sheet))
891
+ hidden.push(entry);
892
+ });
893
+ worked = { sheet: behind.sheet, nodes: behind.nodes, entries: everything, hidden };
894
+ return worked;
895
+ };
896
+ }
897
+ return snapshot;
898
+ }
899
+ /** The height of iOS's tab bar on an iPhone in portrait, in points (the classic and iOS 26 bars). */
900
+ const TAB_BAR_HEIGHT = 49;
901
+ /** The home-indicator inset, from the app's safe-area library when it has one. */
902
+ function bottomInset(screen) {
903
+ try {
904
+ const safeArea = require('react-native-safe-area-context');
905
+ const metrics = (0, types_1.isRecord)(safeArea) ? safeArea.initialWindowMetrics : undefined;
906
+ const insets = (0, types_1.isRecord)(metrics) ? metrics.insets : undefined;
907
+ if ((0, types_1.isRecord)(insets) && typeof insets.bottom === 'number' && insets.bottom >= 0)
908
+ return insets.bottom;
909
+ }
910
+ catch {
911
+ // not installed
912
+ }
913
+ return screen.height >= 812 ? 34 : 0; // iPhones with Face ID have a home indicator
914
+ }
915
+ /**
916
+ * The native tab bar, which iOS draws outside React's tree: on an iPhone in portrait, 49 points
917
+ * above the home-indicator inset, its items spread evenly across the width (the classic and the
918
+ * iOS 26 bars alike). Null without native tabs, on other platforms, in landscape, and while a
919
+ * modal's window covers it.
920
+ */
921
+ function nativeTabBar(snapshot) {
922
+ const tabs = snapshot.entries.filter((entry) => entry.tab !== undefined);
923
+ if (tabs.length === 0)
924
+ return null;
925
+ if (react_native_1.Platform.OS !== 'ios' || react_native_1.Platform.isPad)
926
+ return null;
927
+ const { width, height } = snapshot.screen;
928
+ if (height <= width || modalOpen())
929
+ return null;
930
+ return { top: height - bottomInset(snapshot.screen) - TAB_BAR_HEIGHT, tabs };
931
+ }
932
+ /** The tab under x on the bar. */
933
+ function tabAt(bar, x, width) {
934
+ const index = Math.floor(x / (width / bar.tabs.length));
935
+ return bar.tabs[Math.min(bar.tabs.length - 1, Math.max(0, index))];
936
+ }
937
+ /** The height of the navigation bar iOS draws under the status bar, and what a large title adds. */
938
+ const NAV_BAR_HEIGHT = 44;
939
+ const NAV_BAR_LARGE_TITLE = 52;
940
+ /** The status-bar (top safe-area) inset, from the app's safe-area library when it has one. */
941
+ function topInset(screen) {
942
+ try {
943
+ const safeArea = require('react-native-safe-area-context');
944
+ const metrics = (0, types_1.isRecord)(safeArea) ? safeArea.initialWindowMetrics : undefined;
945
+ const insets = (0, types_1.isRecord)(metrics) ? metrics.insets : undefined;
946
+ if ((0, types_1.isRecord)(insets) && typeof insets.top === 'number' && insets.top >= 0)
947
+ return insets.top;
948
+ }
949
+ catch {
950
+ // not installed
951
+ }
952
+ if (screen.height >= 852)
953
+ return 59;
954
+ return screen.height >= 812 ? 47 : 20;
955
+ }
956
+ function isHostType(fiber, type) {
957
+ return fiber.tag === fiber_1.HostComponent && fiber.type === type;
958
+ }
959
+ /** The screens of a stack (nearest `RNSScreen`s, in order), looking through wrappers. */
960
+ function stackScreens(stack) {
961
+ const screens = [];
962
+ const pending = stack.child ? [stack.child] : [];
963
+ for (let fiber = pending.pop(); fiber; fiber = pending.pop()) {
964
+ if (fiber.sibling)
965
+ pending.push(fiber.sibling);
966
+ if (isHostType(fiber, 'RNSScreen'))
967
+ screens.push(fiber);
968
+ else if (!isHostType(fiber, 'RNSScreenStack') && fiber.child)
969
+ pending.push(fiber.child);
970
+ }
971
+ return screens;
972
+ }
973
+ /**
974
+ * The native navigation bar react-native-screens draws for the visible screen, outside React's
975
+ * layout. The visible header is the deepest `RNSScreenStackHeaderConfig` reached by walking each
976
+ * root and, at an `RNSScreenStack`, descending only into its last (top) `RNSScreen`. Null without
977
+ * one, with `hidden` set, on other platforms, in landscape, and while a modal's window covers it.
978
+ */
979
+ function nativeNavBar(snapshot) {
980
+ if (react_native_1.Platform.OS !== 'ios' || react_native_1.Platform.isPad)
981
+ return null;
982
+ const { width, height } = snapshot.screen;
983
+ if (height <= width || modalOpen())
984
+ return null;
985
+ let found = null;
986
+ for (const root of (0, register_1.getRoots)()) {
987
+ const todo = root.current.child
988
+ ? [{ fiber: root.current.child, context: { firstScreen: true } }]
989
+ : [];
990
+ for (let item = todo.pop(); item; item = todo.pop()) {
991
+ const { fiber, context } = item;
992
+ if (fiber.sibling)
993
+ todo.push({ fiber: fiber.sibling, context });
994
+ // The same rule `look` uses: unselected native tabs, inactive screens and hidden subtrees don't count.
995
+ if (fiber.tag === fiber_1.HostComponent && typeof fiber.type === 'string' && hidesSubtree(fiber.type, (0, fiber_1.propsOf)(fiber)))
996
+ continue;
997
+ if (isHostType(fiber, 'RNSScreenStackHeaderConfig')) {
998
+ const props = (0, fiber_1.propsOf)(fiber);
999
+ if (props.hidden !== true)
1000
+ found = { header: fiber, context };
1001
+ }
1002
+ if (isHostType(fiber, 'RNSScreenStack')) {
1003
+ const screens = stackScreens(fiber);
1004
+ const top = screens[screens.length - 1];
1005
+ if (top?.child)
1006
+ todo.push({ fiber: top.child, context: { firstScreen: screens.length === 1 } });
1007
+ continue;
1008
+ }
1009
+ if (fiber.child) {
1010
+ todo.push({ fiber: fiber.child, context: isHostType(fiber, 'RNSScreen') ? { firstScreen: true } : context });
1011
+ }
1012
+ }
1013
+ }
1014
+ if (!found)
1015
+ return null;
1016
+ const props = (0, fiber_1.propsOf)(found.header);
1017
+ const bottom = topInset(snapshot.screen) + NAV_BAR_HEIGHT + (props.largeTitle === true ? NAV_BAR_LARGE_TITLE : 0);
1018
+ const bar = {
1019
+ top: 0,
1020
+ bottom,
1021
+ canGoBack: !found.context.firstScreen && props.hideBackButton !== true,
1022
+ header: found.header,
1023
+ };
1024
+ if (typeof props.title === 'string')
1025
+ bar.title = props.title;
1026
+ return bar;
1027
+ }
1028
+ /** Whether a fiber is drawn inside the navigation bar (a custom header item): it stays reachable. */
1029
+ function inNavBarHeader(bar, fiber) {
1030
+ return isWithin(fiber, bar.header);
1031
+ }
1032
+ let refCounter = 0;
1033
+ const refToFiber = new Map();
1034
+ const fiberToRef = new Map();
1035
+ function bindRef(ref, fiber) {
1036
+ refToFiber.set(ref, fiber);
1037
+ fiberToRef.set(fiber, ref);
1038
+ if (fiber.alternate)
1039
+ fiberToRef.set(fiber.alternate, ref);
1040
+ }
1041
+ function resetRefs() {
1042
+ refCounter = 0;
1043
+ refToFiber.clear();
1044
+ fiberToRef.clear();
1045
+ }
1046
+ /** The ref for a fiber in the remembered snapshot, allocating a new one when unseen. */
1047
+ function refFor(fiber) {
1048
+ const direct = fiberToRef.get(fiber) ?? (fiber.alternate ? fiberToRef.get(fiber.alternate) : undefined);
1049
+ if (direct)
1050
+ return direct;
1051
+ for (const [ref, known] of refToFiber) {
1052
+ if ((0, fiber_1.sameFiber)(known, fiber)) {
1053
+ bindRef(ref, fiber);
1054
+ return ref;
1055
+ }
1056
+ }
1057
+ const ref = `e${++refCounter}`;
1058
+ bindRef(ref, fiber);
1059
+ return ref;
1060
+ }
1061
+ /** The ref a fiber already has, without handing out a new one. */
1062
+ function peekRef(fiber) {
1063
+ const direct = fiberToRef.get(fiber) ?? (fiber.alternate ? fiberToRef.get(fiber.alternate) : undefined);
1064
+ if (direct)
1065
+ return direct;
1066
+ for (const [ref, known] of refToFiber)
1067
+ if ((0, fiber_1.sameFiber)(known, fiber))
1068
+ return ref;
1069
+ return null;
1070
+ }
1071
+ /**
1072
+ * `overlay`: the elements on screen with their frames and the UI revision, for the live
1073
+ * viewer's boxes. Unlike `tree` it leaves refs and the change baseline alone.
1074
+ */
1075
+ async function overlay() {
1076
+ const rev = (0, register_1.commitRevision)();
1077
+ const snapshot = takeSnapshot();
1078
+ const candidates = snapshot.entries.filter((entry) => entry.info.interesting && !entry.tab);
1079
+ const frames = await Promise.all(candidates.map((entry) => measureFiber(entry.node.fiber)));
1080
+ const elements = [];
1081
+ candidates.forEach((entry, i) => {
1082
+ const frame = frames[i];
1083
+ if (!frame || !isOnScreen(frame, snapshot.screen))
1084
+ return;
1085
+ const { info } = entry;
1086
+ elements.push({
1087
+ ref: peekRef(entry.node.fiber),
1088
+ type: roleName(info),
1089
+ label: info.label ?? (info.text !== '' ? info.text.slice(0, 80) : null),
1090
+ id: info.testID,
1091
+ frame,
1092
+ });
1093
+ });
1094
+ return { rev, screen: snapshot.screen, elements };
1095
+ }
1096
+ function fiberForRef(ref) {
1097
+ return refToFiber.get(ref) ?? null;
1098
+ }
1099
+ /** An entry as an element. `ref` is the fiber's ref unless one is passed ('' for an element that isn't listed). */
1100
+ function toElement(entry, frame, ref = refFor(entry.node.fiber)) {
1101
+ const { info } = entry;
1102
+ const kept = info.text.length > exports.MAX_KEPT_TEXT ? sliceWhole(info.text, exports.MAX_KEPT_TEXT) : info.text;
1103
+ const more = info.text.length - kept.length;
1104
+ return {
1105
+ ref,
1106
+ type: info.type,
1107
+ role: info.role,
1108
+ testID: info.testID,
1109
+ label: info.label,
1110
+ text: kept,
1111
+ ...(more > 0 ? { textMore: more } : {}),
1112
+ value: info.value === null ? null : displayText(info.value),
1113
+ ...(info.placeholder !== null && displayText(info.placeholder) !== '' ? { placeholder: displayText(info.placeholder) } : {}),
1114
+ frame,
1115
+ actions: info.actions,
1116
+ disabled: info.disabled,
1117
+ selected: info.selected,
1118
+ checked: info.checked,
1119
+ };
1120
+ }
1121
+ /**
1122
+ * The previous `tree` result, keyed by host instance, for `changes`: what was listed, what lay
1123
+ * above or below the screen, and whether the keyboard was up (undefined: not known).
1124
+ */
1125
+ let lastTree = null;
1126
+ /** How many changed off-screen elements a change block lists one by one (AGENT-TOOLS v1.6). */
1127
+ const MAX_OFF_SCREEN_CHANGES = 5;
1128
+ /** Forget the previous tree (tests; a new connection). */
1129
+ function forgetLastTree() {
1130
+ lastTree = null;
1131
+ }
1132
+ /** A key that survives re-renders: Fabric clones `stateNode` on updates but keeps `canonical`. */
1133
+ function stableKey(entry) {
1134
+ return stableObjectKey(entry.node.fiber);
1135
+ }
1136
+ function framesDiffer(a, b) {
1137
+ if (a === null || b === null)
1138
+ return a !== b;
1139
+ return (Math.abs(a.x - b.x) > 1 || Math.abs(a.y - b.y) > 1 || Math.abs(a.width - b.width) > 1 || Math.abs(a.height - b.height) > 1);
1140
+ }
1141
+ /**
1142
+ * Whether an element's line shows its label apart from its text (`label="…"`): only when both are
1143
+ * there and they differ (ignoring case). A label that just repeats the text is not a second value.
1144
+ * With no text, the label is what the line names the element by, so it is shown.
1145
+ */
1146
+ function showsLabel(element) {
1147
+ const label = element.label ?? '';
1148
+ if (label === '')
1149
+ return false;
1150
+ const text = element.text;
1151
+ return text === '' || label.toLowerCase() !== text.toLowerCase();
1152
+ }
1153
+ function changedFields(old, now) {
1154
+ const was = {};
1155
+ if (old.text !== now.text || old.textMore !== now.textMore)
1156
+ was.text = old.text;
1157
+ // A label counts as changed only when the line shows it, before or after.
1158
+ if (old.label !== now.label && (showsLabel(old) || showsLabel(now)))
1159
+ was.label = old.label;
1160
+ if (old.value !== now.value)
1161
+ was.value = old.value;
1162
+ if (old.placeholder !== now.placeholder && old.placeholder !== undefined)
1163
+ was.placeholder = old.placeholder;
1164
+ if (old.checked !== now.checked)
1165
+ was.checked = old.checked;
1166
+ if (old.selected !== now.selected)
1167
+ was.selected = old.selected;
1168
+ if (old.disabled !== now.disabled)
1169
+ was.disabled = old.disabled;
1170
+ if (old.actions.join(',') !== now.actions.join(','))
1171
+ was.actions = old.actions;
1172
+ return was;
1173
+ }
1174
+ function diffTrees(previous, byKey, offKey, route) {
1175
+ const changes = { sameRoute: previous.route === route, added: [], removed: [], changed: [], moved: [] };
1176
+ // Off screen now: only what changed in it counts (a scroll moves all of them, and a lazy list
1177
+ // makes and drops rows out of view all the time). It may have been off screen before too, or on
1178
+ // the list and scrolled away since (an action answered without a list, then this one).
1179
+ let offScreenChanged = 0;
1180
+ for (const [key, now] of offKey) {
1181
+ const old = previous.offKey.get(key) ?? previous.byKey.get(key);
1182
+ if (!old)
1183
+ continue;
1184
+ // The same element: the same identifier, or the same role in the same place (Tapwright's rule
1185
+ // too). A recycling list hands an off-screen cell to another row; that is not a change.
1186
+ const sameId = now.testID !== null && now.testID === old.testID;
1187
+ const samePlace = now.role === old.role && now.type === old.type && !framesDiffer(old.frame, now.frame);
1188
+ if (!sameId && !samePlace)
1189
+ continue;
1190
+ const { actions: _actions, ...was } = changedFields(old, now);
1191
+ if (Object.keys(was).length === 0)
1192
+ continue;
1193
+ offScreenChanged++;
1194
+ if (offScreenChanged <= MAX_OFF_SCREEN_CHANGES)
1195
+ (changes.offScreen ??= []).push({ element: now, was });
1196
+ }
1197
+ if (offScreenChanged > MAX_OFF_SCREEN_CHANGES)
1198
+ changes.offScreenMore = offScreenChanged - MAX_OFF_SCREEN_CHANGES;
1199
+ for (const [key, now] of byKey) {
1200
+ const old = previous.byKey.get(key);
1201
+ if (!old) {
1202
+ changes.added.push(now.ref);
1203
+ continue;
1204
+ }
1205
+ const was = changedFields(old, now);
1206
+ if (Object.keys(was).length > 0)
1207
+ changes.changed.push({ ref: now.ref, was });
1208
+ else if (framesDiffer(old.frame, now.frame))
1209
+ changes.moved.push(now.ref);
1210
+ }
1211
+ for (const [key, old] of previous.byKey)
1212
+ if (!byKey.has(key))
1213
+ changes.removed.push(old);
1214
+ return changes;
1215
+ }
1216
+ /** What an element line quotes, for comparing two elements: the text, else the label, whitespace flattened, its first MAX_TEXT characters. */
1217
+ function quotedText(entry) {
1218
+ const { info } = entry;
1219
+ const flat = (info.text !== '' ? info.text : (info.label ?? '')).replace(/\s+/g, ' ').trim();
1220
+ return flat.length > exports.MAX_TEXT ? flat.slice(0, exports.MAX_TEXT) : flat;
1221
+ }
1222
+ function sameRect(a, b) {
1223
+ return (Math.abs(a.x - b.x) <= 1 && Math.abs(a.y - b.y) <= 1 && Math.abs(a.width - b.width) <= 1 && Math.abs(a.height - b.height) <= 1);
1224
+ }
1225
+ /**
1226
+ * AGENT-TOOLS v1.3 "Duplicates": when an element and its nearest listed ancestor have the same
1227
+ * quoted text and the same rect (within 1 pt), only one is listed: the one that takes an action,
1228
+ * else (both or neither do) the ancestor. Roles may differ: a labelled wrapper around an input
1229
+ * lists only the input. Elements are in tree order, so an ancestor comes before its descendants.
1230
+ */
1231
+ function foldDuplicates(snapshot, listed) {
1232
+ const byNode = new Map();
1233
+ for (const item of listed)
1234
+ byNode.set(item.entry.node.index, item);
1235
+ const dropped = new Set();
1236
+ for (const item of listed) {
1237
+ if (item.frame === null)
1238
+ continue;
1239
+ const text = quotedText(item.entry);
1240
+ if (text === '')
1241
+ continue;
1242
+ let ancestor;
1243
+ for (let p = item.entry.node.parent; p >= 0; p = snapshot.nodes[p]?.parent ?? -1) {
1244
+ const candidate = byNode.get(p);
1245
+ if (candidate && !dropped.has(candidate)) {
1246
+ ancestor = candidate;
1247
+ break;
1248
+ }
1249
+ }
1250
+ if (!ancestor || ancestor.frame === null)
1251
+ continue;
1252
+ if (!sameRect(item.frame, ancestor.frame) || quotedText(ancestor.entry) !== text)
1253
+ continue;
1254
+ const itemActs = item.entry.info.actions.length > 0;
1255
+ const ancestorActs = ancestor.entry.info.actions.length > 0;
1256
+ dropped.add(itemActs && !ancestorActs ? ancestor : item);
1257
+ }
1258
+ return dropped.size === 0 ? listed : listed.filter((item) => !dropped.has(item));
1259
+ }
1260
+ /**
1261
+ * `tree`: the screen as elements. A look (no `changes`) starts the refs at e1 again. With
1262
+ * `changes: true`, which the host asks for in an action's answer, elements still on screen keep
1263
+ * their refs and new ones count on from the highest ref handed out since the last look, so an old
1264
+ * ref never lands on another element; it also reports what changed since the previous call. A new
1265
+ * host session has no previous call (`forgetLastTree`), so its first list starts over like a look.
1266
+ */
1267
+ async function tree(params = {}) {
1268
+ const all = (0, types_1.isRecord)(params) && params.all === true;
1269
+ // `peek`: a list for the host's own use (naming what a picture shows). The agent isn't given it,
1270
+ // so its refs keep their meaning and the baseline for changes stays where it is.
1271
+ const peek = (0, types_1.isRecord)(params) && params.peek === true;
1272
+ const wantChanges = !peek && (0, types_1.isRecord)(params) && params.changes === true && lastTree !== null;
1273
+ // An action may have opened (or closed) an alert or the share sheet UIKit draws: read it again.
1274
+ await (0, native_1.refreshCover)();
1275
+ const snapshot = takeSnapshot();
1276
+ const candidates = snapshot.entries.filter((entry) => all || entry.info.interesting);
1277
+ const frames = await Promise.all(candidates.map((entry) => measureFiber(entry.node.fiber)));
1278
+ if (!wantChanges && !peek)
1279
+ resetRefs();
1280
+ const elements = [];
1281
+ const byKey = new Map();
1282
+ let listed = [];
1283
+ // Elements with a size that lie wholly above or below the screen: counted, not listed (v1.5).
1284
+ let above = [];
1285
+ let below = [];
1286
+ candidates.forEach((entry, i) => {
1287
+ const frame = entry.tab ? null : (frames[i] ?? null);
1288
+ if (!all && frame !== null && !isOnScreen(frame, snapshot.screen)) {
1289
+ if (frame.width > 0 && frame.height > 0) {
1290
+ if (frame.y + frame.height <= 0)
1291
+ above.push({ entry, frame });
1292
+ else if (frame.y >= snapshot.screen.height)
1293
+ below.push({ entry, frame });
1294
+ }
1295
+ return;
1296
+ }
1297
+ listed.push({ entry, frame });
1298
+ });
1299
+ if (!all) {
1300
+ listed = foldDuplicates(snapshot, listed);
1301
+ above = foldDuplicates(snapshot, above);
1302
+ below = foldDuplicates(snapshot, below);
1303
+ }
1304
+ for (const { entry, frame } of listed) {
1305
+ const element = peek ? toElement(entry, frame, peekRef(entry.node.fiber) ?? '') : toElement(entry, frame);
1306
+ elements.push(element);
1307
+ byKey.set(stableKey(entry), element);
1308
+ }
1309
+ const result = { screen: snapshot.screen, elements };
1310
+ if (above.length > 0 || below.length > 0)
1311
+ result.offScreen = { above: above.length, below: below.length };
1312
+ // What lies above and below, without refs (it isn't listed): the baseline for changes off screen.
1313
+ const offKey = new Map();
1314
+ for (const { entry, frame } of [...above, ...below])
1315
+ offKey.set(stableKey(entry), toElement(entry, frame, ''));
1316
+ const current = currentRoute(snapshot.routes);
1317
+ if (current) {
1318
+ const params = simpleParams(current.params);
1319
+ result.route = params ? { names: current.chain, params } : { names: current.chain };
1320
+ }
1321
+ const alerts = (0, alerts_1.openAlerts)();
1322
+ if (alerts.length > 0) {
1323
+ result.alerts = alerts.map(({ kind, title, message, buttons, standIn }) => ({ kind, title, message, buttons, standIn }));
1324
+ }
1325
+ const busy = (0, network_1.inFlight)();
1326
+ if (busy > 0)
1327
+ result.network = { inFlight: busy };
1328
+ // `Keyboard: shown` in the header while it is up; in a change block only when it came or went.
1329
+ const keyboard = (0, keyboard_1.keyboardState)()?.shown;
1330
+ if (keyboard === true)
1331
+ result.keyboard = true;
1332
+ const routeKey = current ? `${current.chain.join('/')}#${current.key}` : '';
1333
+ if (wantChanges && lastTree !== null) {
1334
+ result.changes = diffTrees(lastTree, byKey, offKey, routeKey);
1335
+ if (keyboard !== undefined && lastTree.keyboard !== undefined && keyboard !== lastTree.keyboard) {
1336
+ result.changes.keyboard = keyboard ? 'shown' : 'hidden';
1337
+ }
1338
+ }
1339
+ if (!peek)
1340
+ lastTree = { byKey, offKey, route: routeKey, keyboard };
1341
+ return result;
1342
+ }
1343
+ //# sourceMappingURL=inspect.js.map