react-native-ai-devtools 1.4.18 → 1.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/build/__tests__/integration/tools.test.js +9 -10
- package/build/__tests__/integration/tools.test.js.map +1 -1
- package/build/core/connection.d.ts +8 -0
- package/build/core/connection.d.ts.map +1 -1
- package/build/core/connection.js +77 -5
- package/build/core/connection.js.map +1 -1
- package/build/core/executor.d.ts +44 -7
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +603 -121
- package/build/core/executor.js.map +1 -1
- package/build/core/index.d.ts +2 -4
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +2 -6
- package/build/core/index.js.map +1 -1
- package/build/index.js +114 -143
- package/build/index.js.map +1 -1
- package/build/pro/tap.js +2 -2
- package/package.json +2 -3
- package/build/core/httpServer.d.ts +0 -14
- package/build/core/httpServer.d.ts.map +0 -1
- package/build/core/httpServer.js +0 -2459
- package/build/core/httpServer.js.map +0 -1
- package/build/core/httpServerProcess.d.ts +0 -25
- package/build/core/httpServerProcess.d.ts.map +0 -1
- package/build/core/httpServerProcess.js +0 -153
- package/build/core/httpServerProcess.js.map +0 -1
- package/build/httpServerStandalone.d.ts +0 -7
- package/build/httpServerStandalone.d.ts.map +0 -1
- package/build/httpServerStandalone.js +0 -31
- package/build/httpServerStandalone.js.map +0 -1
package/build/core/executor.js
CHANGED
|
@@ -531,19 +531,184 @@ function formatTreeStructureOnly(node, indent = 0) {
|
|
|
531
531
|
}
|
|
532
532
|
return result;
|
|
533
533
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Classify each root as "screen" or "overlay".
|
|
536
|
+
*
|
|
537
|
+
* A root is a **screen** if its subtree contains a navigation screen marker
|
|
538
|
+
* (Route(...), *Screen, *Page). Falls back to the largest root by area
|
|
539
|
+
* if no navigation markers are found. Everything else is an overlay.
|
|
540
|
+
*/
|
|
541
|
+
function classifyRoots(roots, elements, childrenMap) {
|
|
542
|
+
const labels = [];
|
|
543
|
+
// Navigation screen markers: Route(...) wrapper or user-defined *Screen/*Page
|
|
544
|
+
const screenMarker = /^(Route\(|.*Screen\(|.*Page\(|.*Screen$|.*Page$)/;
|
|
545
|
+
function subtreeHasScreen(idx, depth) {
|
|
546
|
+
if (depth > 30)
|
|
547
|
+
return false;
|
|
548
|
+
if (screenMarker.test(elements[idx].component))
|
|
549
|
+
return true;
|
|
550
|
+
const kids = childrenMap.get(idx);
|
|
551
|
+
if (kids) {
|
|
552
|
+
for (const kid of kids) {
|
|
553
|
+
if (subtreeHasScreen(kid, depth + 1))
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
// First pass: check which roots contain a navigation screen
|
|
560
|
+
const hasScreen = roots.map(ri => subtreeHasScreen(ri, 0));
|
|
561
|
+
const anyScreenFound = hasScreen.some(Boolean);
|
|
562
|
+
if (anyScreenFound) {
|
|
563
|
+
for (let i = 0; i < roots.length; i++) {
|
|
564
|
+
labels.push(hasScreen[i] ? "screen" : "overlay");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Fallback: largest root by area is the screen
|
|
569
|
+
let maxArea = 0;
|
|
570
|
+
let maxIdx = 0;
|
|
571
|
+
for (let i = 0; i < roots.length; i++) {
|
|
572
|
+
const f = elements[roots[i]].frame;
|
|
573
|
+
if (f && f.width * f.height > maxArea) {
|
|
574
|
+
maxArea = f.width * f.height;
|
|
575
|
+
maxIdx = i;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (let i = 0; i < roots.length; i++) {
|
|
579
|
+
labels.push(i === maxIdx ? "screen" : "overlay");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { labels, hasOverlays: labels.some(l => l === "overlay") };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Build, collapse, classify, and render a layout tree.
|
|
586
|
+
*
|
|
587
|
+
* Shared by both get_screen_layout (points, no tap coords) and
|
|
588
|
+
* the screenshot layout enrichment (pixels, with tap coords).
|
|
589
|
+
*
|
|
590
|
+
* @param elements - flat list of layout nodes with parentIndex linkage
|
|
591
|
+
* @param renderLine - callback that produces the output line for a node,
|
|
592
|
+
* given (element, indent level, whether it's a leaf)
|
|
593
|
+
*/
|
|
594
|
+
function formatLayoutTree(elements, renderLine) {
|
|
595
|
+
// Build index: originalIndex -> element index in the filtered array
|
|
596
|
+
const indexMap = new Map();
|
|
597
|
+
for (let i = 0; i < elements.length; i++) {
|
|
598
|
+
if (elements[i].originalIndex !== undefined) {
|
|
599
|
+
indexMap.set(elements[i].originalIndex, i);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Build children lists
|
|
603
|
+
const children = new Map();
|
|
604
|
+
const roots = [];
|
|
605
|
+
for (let i = 0; i < elements.length; i++) {
|
|
606
|
+
const el = elements[i];
|
|
607
|
+
const parentOrigIdx = el.parentIndex;
|
|
608
|
+
if (parentOrigIdx === undefined || parentOrigIdx === -1 || !indexMap.has(parentOrigIdx)) {
|
|
609
|
+
roots.push(i);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const parentIdx = indexMap.get(parentOrigIdx);
|
|
613
|
+
if (!children.has(parentIdx))
|
|
614
|
+
children.set(parentIdx, []);
|
|
615
|
+
children.get(parentIdx).push(i);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Collapse wrapper chains: if a child has the same frame as its parent,
|
|
619
|
+
// it's just a wrapper — promote its children and text to the parent
|
|
620
|
+
function sameFrame(a, b) {
|
|
621
|
+
if (!a.frame || !b.frame)
|
|
622
|
+
return false;
|
|
623
|
+
return Math.abs(a.frame.x - b.frame.x) < 1 &&
|
|
624
|
+
Math.abs(a.frame.y - b.frame.y) < 1 &&
|
|
625
|
+
Math.abs(a.frame.width - b.frame.width) < 1 &&
|
|
626
|
+
Math.abs(a.frame.height - b.frame.height) < 1;
|
|
627
|
+
}
|
|
628
|
+
function collapseNode(parentIdx) {
|
|
629
|
+
const kids = children.get(parentIdx);
|
|
630
|
+
if (!kids)
|
|
631
|
+
return;
|
|
632
|
+
const newKids = [];
|
|
633
|
+
for (const kidIdx of kids) {
|
|
634
|
+
if (sameFrame(elements[parentIdx], elements[kidIdx])) {
|
|
635
|
+
if (!elements[parentIdx].text && elements[kidIdx].text) {
|
|
636
|
+
elements[parentIdx].text = elements[kidIdx].text;
|
|
637
|
+
}
|
|
638
|
+
const grandKids = children.get(kidIdx);
|
|
639
|
+
if (grandKids) {
|
|
640
|
+
newKids.push(...grandKids);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
newKids.push(kidIdx);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (newKids.length > 0) {
|
|
648
|
+
children.set(parentIdx, newKids);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
children.delete(parentIdx);
|
|
652
|
+
}
|
|
653
|
+
const updatedKids = children.get(parentIdx);
|
|
654
|
+
if (updatedKids) {
|
|
655
|
+
for (const kid of updatedKids) {
|
|
656
|
+
collapseNode(kid);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const root of roots) {
|
|
661
|
+
collapseNode(root);
|
|
662
|
+
}
|
|
663
|
+
// Render tree
|
|
664
|
+
const lines = [];
|
|
665
|
+
function printNode(idx, indent) {
|
|
666
|
+
const isLeaf = !children.has(idx);
|
|
667
|
+
lines.push(renderLine(elements[idx], indent, isLeaf));
|
|
668
|
+
const kids = children.get(idx);
|
|
669
|
+
if (kids) {
|
|
670
|
+
for (const kid of kids) {
|
|
671
|
+
printNode(kid, indent + 1);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Classify roots and emit layer headers
|
|
676
|
+
const { labels, hasOverlays } = classifyRoots(roots, elements, children);
|
|
677
|
+
if (hasOverlays) {
|
|
678
|
+
let prevLabel = "";
|
|
679
|
+
for (let ri = 0; ri < roots.length; ri++) {
|
|
680
|
+
const label = labels[ri];
|
|
681
|
+
if (label === "overlay" || label !== prevLabel) {
|
|
682
|
+
if (lines.length > 0)
|
|
683
|
+
lines.push("");
|
|
684
|
+
lines.push(`[${label}]`);
|
|
685
|
+
}
|
|
686
|
+
prevLabel = label;
|
|
687
|
+
printNode(roots[ri], 0);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
for (const root of roots) {
|
|
692
|
+
printNode(root, 0);
|
|
693
|
+
}
|
|
544
694
|
}
|
|
545
695
|
return lines.join("\n");
|
|
546
696
|
}
|
|
697
|
+
function formatScreenLayoutTree(elements, extended = false) {
|
|
698
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
699
|
+
const prefix = " ".repeat(indent);
|
|
700
|
+
const frame = el.frame
|
|
701
|
+
? ` (${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`
|
|
702
|
+
: "";
|
|
703
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
704
|
+
const idStr = id ? ` [${id}]` : "";
|
|
705
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
706
|
+
const layoutStr = extended && el.layout
|
|
707
|
+
? ` {${Object.entries(el.layout).map(([k, v]) => `${k}:${v}`).join("; ")}}`
|
|
708
|
+
: "";
|
|
709
|
+
return `${prefix}${el.component}${frame}${idStr}${textStr}${layoutStr}`;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
547
712
|
function formatFoundComponentsToTonl(components) {
|
|
548
713
|
const lines = ["#found{component,path,depth,key,layout}"];
|
|
549
714
|
for (const c of components) {
|
|
@@ -572,7 +737,7 @@ export async function getComponentTree(options = {}) {
|
|
|
572
737
|
// Use lower default depth for structureOnly to keep output compact (~2-5KB)
|
|
573
738
|
// Full mode uses higher depth since TONL format handles it better
|
|
574
739
|
// focusedOnly mode uses moderate depth since we're already filtering to active screen
|
|
575
|
-
const maxDepth = options.maxDepth ??
|
|
740
|
+
const maxDepth = options.maxDepth ?? 5000;
|
|
576
741
|
const expression = `
|
|
577
742
|
(function() {
|
|
578
743
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -717,7 +882,7 @@ export async function getComponentTree(options = {}) {
|
|
|
717
882
|
|
|
718
883
|
// Find focused screen if requested
|
|
719
884
|
function findFocusedScreen(fiber, depth = 0) {
|
|
720
|
-
if (!fiber || depth >
|
|
885
|
+
if (!fiber || depth > 5000) return null;
|
|
721
886
|
|
|
722
887
|
const name = getComponentName(fiber);
|
|
723
888
|
|
|
@@ -793,33 +958,57 @@ export async function getComponentTree(options = {}) {
|
|
|
793
958
|
return result;
|
|
794
959
|
}
|
|
795
960
|
/**
|
|
796
|
-
* Get layout
|
|
797
|
-
*
|
|
961
|
+
* Get layout data for visible components on the current screen.
|
|
962
|
+
* Uses measureInWindow to get actual screen positions and filters
|
|
963
|
+
* to only components within the viewport.
|
|
964
|
+
*
|
|
965
|
+
* Two-step approach (same as inspectAtPoint):
|
|
966
|
+
* Step 1: Walk fiber tree, dispatch measureInWindow on host components
|
|
967
|
+
* Step 2: After 300ms, read measurements, filter by viewport, build results
|
|
798
968
|
*/
|
|
799
969
|
export async function getScreenLayout(options = {}) {
|
|
800
|
-
const {
|
|
801
|
-
const
|
|
970
|
+
const { extended = false, summary = false, device, raw = false } = options;
|
|
971
|
+
const maxDepth = 5000;
|
|
972
|
+
const componentsOnly = true;
|
|
973
|
+
const shortPath = true;
|
|
974
|
+
// --- Step 1: walk fiber tree + dispatch measureInWindow calls ---
|
|
975
|
+
const dispatchExpression = `
|
|
802
976
|
(function() {
|
|
803
|
-
|
|
977
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
804
978
|
if (!hook) return { error: 'React DevTools hook not found.' };
|
|
805
979
|
|
|
806
|
-
|
|
980
|
+
var roots = [];
|
|
807
981
|
if (hook.getFiberRoots) {
|
|
808
|
-
roots =
|
|
982
|
+
roots = Array.from(hook.getFiberRoots(1) || []);
|
|
809
983
|
}
|
|
810
984
|
if (roots.length === 0 && hook.renderers) {
|
|
811
|
-
for (
|
|
812
|
-
|
|
985
|
+
for (var entry of hook.renderers) {
|
|
986
|
+
var r = Array.from(hook.getFiberRoots ? (hook.getFiberRoots(entry[0]) || []) : []);
|
|
813
987
|
if (r.length > 0) { roots = r; break; }
|
|
814
988
|
}
|
|
815
989
|
}
|
|
816
990
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
817
991
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
992
|
+
function getMeasurable(fiber) {
|
|
993
|
+
var sn = fiber.stateNode;
|
|
994
|
+
if (!sn) return null;
|
|
995
|
+
if (typeof sn.measureInWindow === 'function') return sn;
|
|
996
|
+
if (sn.canonical && sn.canonical.publicInstance &&
|
|
997
|
+
typeof sn.canonical.publicInstance.measureInWindow === 'function') {
|
|
998
|
+
return sn.canonical.publicInstance;
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Collect host fibers with their metadata
|
|
1004
|
+
var hostFibers = [];
|
|
1005
|
+
var fiberMeta = [];
|
|
1006
|
+
var componentsOnlyMode = ${componentsOnly};
|
|
1007
|
+
|
|
1008
|
+
// RN internals and primitives — skip these when looking for meaningful component names
|
|
1009
|
+
// Only filter internal components that can't be written as JSX.
|
|
1010
|
+
// Keep all user-facing components (View, Text, ScrollView, Modal, etc.)
|
|
1011
|
+
var RN_PRIMITIVES = /^(Animated\\(.*|withAnimated.*|AnimatedComponent.*|ForwardRef.*|memo\\(.*|Context\\.Consumer|Context\\.Provider|RCT.*|RNS.*|RNC.*|ViewManagerAdapter_.*|VirtualizedList.*|CellRenderer.*|FrameSizeProvider.*|MaybeScreenContainer|MaybeScreen|Navigation.*|Screen$|ScreenStack|ScreenContainer|ScreenContentWrapper|SceneView|DelayedFreeze|Freeze|Suspender|DebugContainer|StaticContainer|SafeAreaProvider.*|SafeAreaFrameContext|SafeAreaInsetsContext|ExpoRoot|ExpoRootComponent|GestureHandler.*|NativeViewGestureHandler|GestureDetector|PanGestureHandler|Reanimated.*|BottomTabNavigator|TabLayout|RouteNode|Route$|KeyboardProvider|PortalProviderComponent|BottomSheetModalProviderWrapper|ThemeContext|ThemeProvider|TextAncestorContext|PressabilityDebugView|TouchableHighlightImpl|StatusBarOverlay|BottomSheetHostingContainerComponent|BottomSheetGestureHandlersProvider|BottomSheetBackdropContainerComponent|BottomSheetContainerComponent|BottomSheetDraggableViewComponent|BottomSheetHandleContainerComponent|BottomSheetBackgroundContainerComponent|DebuggingOverlay|InspectorDeferred|Inspector|InspectorOverlay|InspectorPanel|StyleInspector|BoxInspector|BoxContainer|ElementBox|BorderBox|InspectorPanelButton)$/;
|
|
823
1012
|
|
|
824
1013
|
function getComponentName(fiber) {
|
|
825
1014
|
if (!fiber || !fiber.type) return null;
|
|
@@ -827,8 +1016,212 @@ export async function getScreenLayout(options = {}) {
|
|
|
827
1016
|
return fiber.type.displayName || fiber.type.name || null;
|
|
828
1017
|
}
|
|
829
1018
|
|
|
830
|
-
|
|
831
|
-
|
|
1019
|
+
// Find the first measurable host descendant of a fiber
|
|
1020
|
+
function findFirstHost(fiber, depth) {
|
|
1021
|
+
if (!fiber || depth > 20) return null;
|
|
1022
|
+
if (typeof fiber.type === 'string' && getMeasurable(fiber)) return fiber;
|
|
1023
|
+
var child = fiber.child;
|
|
1024
|
+
while (child) {
|
|
1025
|
+
var found = findFirstHost(child, depth + 1);
|
|
1026
|
+
if (found) return found;
|
|
1027
|
+
child = child.sibling;
|
|
1028
|
+
}
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Extract text content from a fiber subtree.
|
|
1033
|
+
// When a fiber has a string child, return it without recursing
|
|
1034
|
+
// (avoids duplication from Text > RCTText having the same string).
|
|
1035
|
+
function collectText(fiber, d) {
|
|
1036
|
+
if (!fiber || d > 30) return '';
|
|
1037
|
+
var props = fiber.memoizedProps;
|
|
1038
|
+
if (props) {
|
|
1039
|
+
var ch = props.children;
|
|
1040
|
+
// Leaf text — return without recursing into children fibers
|
|
1041
|
+
if (typeof ch === 'string') return ch;
|
|
1042
|
+
if (typeof ch === 'number') return String(ch);
|
|
1043
|
+
if (Array.isArray(ch)) {
|
|
1044
|
+
var inline = [];
|
|
1045
|
+
for (var ci = 0; ci < ch.length; ci++) {
|
|
1046
|
+
if (typeof ch[ci] === 'string') inline.push(ch[ci]);
|
|
1047
|
+
else if (typeof ch[ci] === 'number') inline.push(String(ch[ci]));
|
|
1048
|
+
}
|
|
1049
|
+
if (inline.length > 0) return inline.join('');
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// No direct text — collect from child fibers (siblings = adjacent elements)
|
|
1053
|
+
var parts = [];
|
|
1054
|
+
var child = fiber.child;
|
|
1055
|
+
while (child) {
|
|
1056
|
+
var t = collectText(child, d + 1);
|
|
1057
|
+
if (t) parts.push(t);
|
|
1058
|
+
child = child.sibling;
|
|
1059
|
+
}
|
|
1060
|
+
return parts.join(' ').trim();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (componentsOnlyMode) {
|
|
1064
|
+
// componentsOnly: walk tree looking for meaningful custom components,
|
|
1065
|
+
// measure their first host child, track parent for tree output
|
|
1066
|
+
function walkComponents(fiber, depth, path, parentIdx) {
|
|
1067
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1068
|
+
var name = getComponentName(fiber);
|
|
1069
|
+
var isHost = typeof fiber.type === 'string';
|
|
1070
|
+
|
|
1071
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1072
|
+
// active: 0 = inactive/detached, 1 = transitioning, 2 = active
|
|
1073
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1074
|
+
|
|
1075
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1076
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1077
|
+
|
|
1078
|
+
var isMeaningful = name && !isHost && !RN_PRIMITIVES.test(name);
|
|
1079
|
+
|
|
1080
|
+
var myIdx = parentIdx;
|
|
1081
|
+
if (isMeaningful) {
|
|
1082
|
+
var host = findFirstHost(fiber, 0);
|
|
1083
|
+
if (host) {
|
|
1084
|
+
myIdx = hostFibers.length;
|
|
1085
|
+
// Extract text from this component's subtree
|
|
1086
|
+
var text = collectText(fiber, 0);
|
|
1087
|
+
hostFibers.push(host);
|
|
1088
|
+
fiberMeta.push({
|
|
1089
|
+
hostName: typeof host.type === 'string' ? host.type : '',
|
|
1090
|
+
customName: name,
|
|
1091
|
+
depth: depth,
|
|
1092
|
+
path: path.concat([name]),
|
|
1093
|
+
parentIndex: parentIdx,
|
|
1094
|
+
text: text ? text.slice(0, 80) : null
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
var child = fiber.child;
|
|
1100
|
+
while (child) {
|
|
1101
|
+
var childName = getComponentName(child);
|
|
1102
|
+
walkComponents(child, depth + 1, childName ? path.concat([childName]) : path, myIdx);
|
|
1103
|
+
child = child.sibling;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
walkComponents(roots[0].current, 0, [], -1);
|
|
1107
|
+
} else {
|
|
1108
|
+
// Default mode: collect all host fibers with ancestor info
|
|
1109
|
+
function walkFibers(fiber, depth, path) {
|
|
1110
|
+
if (!fiber || depth > ${maxDepth}) return;
|
|
1111
|
+
var name = getComponentName(fiber);
|
|
1112
|
+
var isHost = typeof fiber.type === 'string';
|
|
1113
|
+
|
|
1114
|
+
// Skip inactive screens (react-native-screens MaybeScreen with active=0)
|
|
1115
|
+
if (name === 'MaybeScreen' && fiber.memoizedProps && fiber.memoizedProps.active === 0) return;
|
|
1116
|
+
|
|
1117
|
+
// Skip unfocused screens in NativeStackNavigator (SceneView with focused=false)
|
|
1118
|
+
if (name === 'SceneView' && fiber.memoizedProps && fiber.memoizedProps.focused === false) return;
|
|
1119
|
+
|
|
1120
|
+
if (name && isHost && getMeasurable(fiber)) {
|
|
1121
|
+
// Find nearest meaningful custom component ancestor for display
|
|
1122
|
+
var customName = null;
|
|
1123
|
+
var fallbackName = null;
|
|
1124
|
+
var cur = fiber.return;
|
|
1125
|
+
while (cur) {
|
|
1126
|
+
if (cur.type && typeof cur.type !== 'string') {
|
|
1127
|
+
var cName = cur.type.displayName || cur.type.name || null;
|
|
1128
|
+
if (cName) {
|
|
1129
|
+
if (!fallbackName) fallbackName = cName;
|
|
1130
|
+
if (!RN_PRIMITIVES.test(cName)) {
|
|
1131
|
+
customName = cName;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
cur = cur.return;
|
|
1137
|
+
}
|
|
1138
|
+
if (!customName) customName = fallbackName;
|
|
1139
|
+
|
|
1140
|
+
hostFibers.push(fiber);
|
|
1141
|
+
fiberMeta.push({
|
|
1142
|
+
hostName: name,
|
|
1143
|
+
customName: customName,
|
|
1144
|
+
depth: depth,
|
|
1145
|
+
path: path.slice()
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
var child = fiber.child;
|
|
1150
|
+
while (child) {
|
|
1151
|
+
var childName = getComponentName(child);
|
|
1152
|
+
walkFibers(child, depth + 1, childName ? path.concat([childName]) : path);
|
|
1153
|
+
child = child.sibling;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
walkFibers(roots[0].current, 0, []);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (hostFibers.length === 0) return { error: 'No measurable host components found.' };
|
|
1160
|
+
|
|
1161
|
+
// Store fibers and metadata globally for step 2
|
|
1162
|
+
globalThis.__layoutFibers = hostFibers;
|
|
1163
|
+
globalThis.__layoutMeta = fiberMeta;
|
|
1164
|
+
globalThis.__layoutMeasurements = new Array(hostFibers.length).fill(null);
|
|
1165
|
+
|
|
1166
|
+
// Dispatch measureInWindow on all host fibers
|
|
1167
|
+
for (var i = 0; i < hostFibers.length; i++) {
|
|
1168
|
+
try {
|
|
1169
|
+
(function(idx) {
|
|
1170
|
+
getMeasurable(hostFibers[idx]).measureInWindow(function(fx, fy, fw, fh) {
|
|
1171
|
+
globalThis.__layoutMeasurements[idx] = { x: fx, y: fy, width: fw, height: fh };
|
|
1172
|
+
});
|
|
1173
|
+
})(i);
|
|
1174
|
+
} catch(e) {}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return { count: hostFibers.length };
|
|
1178
|
+
})()
|
|
1179
|
+
`;
|
|
1180
|
+
const dispatchResult = await executeInApp(dispatchExpression, false, { timeoutMs: 30000 }, device);
|
|
1181
|
+
if (!dispatchResult.success)
|
|
1182
|
+
return dispatchResult;
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed = JSON.parse(dispatchResult.result || "{}");
|
|
1185
|
+
if (parsed.error)
|
|
1186
|
+
return { success: false, error: parsed.error };
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
/* ignore */
|
|
1190
|
+
}
|
|
1191
|
+
// Wait for measureInWindow callbacks
|
|
1192
|
+
await delay(300);
|
|
1193
|
+
// --- Step 2: read measurements, filter visible, build results ---
|
|
1194
|
+
const resolveExpression = `
|
|
1195
|
+
(function() {
|
|
1196
|
+
var fibers = globalThis.__layoutFibers;
|
|
1197
|
+
var meta = globalThis.__layoutMeta;
|
|
1198
|
+
var measurements = globalThis.__layoutMeasurements;
|
|
1199
|
+
globalThis.__layoutFibers = null;
|
|
1200
|
+
globalThis.__layoutMeta = null;
|
|
1201
|
+
globalThis.__layoutMeasurements = null;
|
|
1202
|
+
|
|
1203
|
+
if (!fibers || !measurements || !meta) {
|
|
1204
|
+
return { error: 'No measurement data. Run get_screen_layout again.' };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
var componentsOnly = ${componentsOnly};
|
|
1208
|
+
var shortPath = ${shortPath};
|
|
1209
|
+
var summaryMode = ${summary};
|
|
1210
|
+
var pathSegments = 3;
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
// Get viewport dimensions from the first root view measurement
|
|
1214
|
+
// Accept elements starting at x=0 even with negative y (safe area extensions)
|
|
1215
|
+
var viewportW = 9999, viewportH = 9999;
|
|
1216
|
+
for (var v = 0; v < measurements.length; v++) {
|
|
1217
|
+
if (measurements[v] && measurements[v].x === 0 && measurements[v].y <= 0 &&
|
|
1218
|
+
measurements[v].width > 0 && measurements[v].height > 0) {
|
|
1219
|
+
viewportW = measurements[v].width;
|
|
1220
|
+
// For wrappers extending behind safe area, the visible viewport height
|
|
1221
|
+
// is the total height minus the negative offset
|
|
1222
|
+
viewportH = measurements[v].height + measurements[v].y;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
832
1225
|
}
|
|
833
1226
|
|
|
834
1227
|
function formatPath(pathArray) {
|
|
@@ -838,22 +1231,14 @@ export async function getScreenLayout(options = {}) {
|
|
|
838
1231
|
return '... > ' + pathArray.slice(-pathSegments).join(' > ');
|
|
839
1232
|
}
|
|
840
1233
|
|
|
841
|
-
function extractAllStyles(style) {
|
|
842
|
-
if (!style) return null;
|
|
843
|
-
const merged = Array.isArray(style)
|
|
844
|
-
? Object.assign({}, ...style.filter(Boolean).map(s => typeof s === 'object' ? s : {}))
|
|
845
|
-
: (typeof style === 'object' ? style : {});
|
|
846
|
-
return Object.keys(merged).length > 0 ? merged : null;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
1234
|
function extractLayoutStyles(style) {
|
|
850
1235
|
if (!style) return null;
|
|
851
|
-
|
|
852
|
-
? Object.assign({}
|
|
1236
|
+
var merged = Array.isArray(style)
|
|
1237
|
+
? Object.assign.apply(null, [{}].concat(style.filter(Boolean).map(function(s) { return typeof s === 'object' ? s : {}; })))
|
|
853
1238
|
: (typeof style === 'object' ? style : {});
|
|
854
1239
|
|
|
855
|
-
|
|
856
|
-
|
|
1240
|
+
var layout = {};
|
|
1241
|
+
var layoutKeys = [
|
|
857
1242
|
'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
|
|
858
1243
|
'paddingHorizontal', 'paddingVertical',
|
|
859
1244
|
'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
|
|
@@ -864,79 +1249,109 @@ export async function getScreenLayout(options = {}) {
|
|
|
864
1249
|
'position', 'top', 'bottom', 'left', 'right',
|
|
865
1250
|
'gap', 'rowGap', 'columnGap',
|
|
866
1251
|
'borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth',
|
|
867
|
-
'backgroundColor', 'borderColor', 'borderRadius'
|
|
1252
|
+
'backgroundColor', 'borderColor', 'borderRadius',
|
|
1253
|
+
'zIndex', 'elevation'
|
|
868
1254
|
];
|
|
869
1255
|
|
|
870
|
-
for (
|
|
871
|
-
if (merged[
|
|
1256
|
+
for (var k = 0; k < layoutKeys.length; k++) {
|
|
1257
|
+
if (merged[layoutKeys[k]] !== undefined) layout[layoutKeys[k]] = merged[layoutKeys[k]];
|
|
872
1258
|
}
|
|
873
1259
|
return Object.keys(layout).length > 0 ? layout : null;
|
|
874
1260
|
}
|
|
875
1261
|
|
|
876
|
-
|
|
1262
|
+
var elements = [];
|
|
1263
|
+
|
|
1264
|
+
for (var i = 0; i < measurements.length; i++) {
|
|
1265
|
+
var m = measurements[i];
|
|
1266
|
+
if (!m) continue;
|
|
877
1267
|
|
|
878
|
-
|
|
879
|
-
if (
|
|
1268
|
+
// Filter: only visible within viewport (with some tolerance for partial visibility)
|
|
1269
|
+
if (m.width <= 0 || m.height <= 0) continue;
|
|
1270
|
+
if (m.x + m.width < 0 || m.y + m.height < 0) continue;
|
|
1271
|
+
if (m.x > viewportW || m.y > viewportH) continue;
|
|
880
1272
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
// Include host components (View, Text, etc.) or named components
|
|
885
|
-
if (name && (!componentsOnly || !isHost)) {
|
|
886
|
-
const style = fiber.memoizedProps?.style;
|
|
887
|
-
const layout = extractLayoutStyles(style);
|
|
888
|
-
|
|
889
|
-
// Get text content if it's a Text component
|
|
890
|
-
let textContent = null;
|
|
891
|
-
if (name === 'Text' || name === 'RCTText') {
|
|
892
|
-
const children = fiber.memoizedProps?.children;
|
|
893
|
-
if (typeof children === 'string') textContent = children;
|
|
894
|
-
else if (typeof children === 'number') textContent = String(children);
|
|
895
|
-
}
|
|
1273
|
+
var fiber = fibers[i];
|
|
1274
|
+
var info = meta[i];
|
|
896
1275
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
depth
|
|
901
|
-
};
|
|
1276
|
+
var displayName = componentsOnly ? info.customName : info.hostName;
|
|
1277
|
+
if (!displayName) continue;
|
|
1278
|
+
if (componentsOnly && !info.customName) continue;
|
|
902
1279
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// Include key props for identification
|
|
907
|
-
if (fiber.memoizedProps) {
|
|
908
|
-
const identifiers = {};
|
|
909
|
-
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
910
|
-
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
911
|
-
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
912
|
-
if (fiber.key) identifiers.key = fiber.key;
|
|
913
|
-
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
914
|
-
}
|
|
1280
|
+
var style = fiber.memoizedProps ? fiber.memoizedProps.style : null;
|
|
1281
|
+
var layout = extractLayoutStyles(style);
|
|
915
1282
|
|
|
916
|
-
|
|
1283
|
+
// Get text content — use pre-collected text from step 1, or fall back to host fiber
|
|
1284
|
+
var textContent = info.text || null;
|
|
1285
|
+
if (!textContent && (info.hostName === 'RCTText' || info.hostName === 'Text')) {
|
|
1286
|
+
var children = fiber.memoizedProps ? fiber.memoizedProps.children : null;
|
|
1287
|
+
if (typeof children === 'string') textContent = children;
|
|
1288
|
+
else if (typeof children === 'number') textContent = String(children);
|
|
917
1289
|
}
|
|
918
1290
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1291
|
+
var element = {
|
|
1292
|
+
component: displayName,
|
|
1293
|
+
path: formatPath(info.path),
|
|
1294
|
+
depth: info.depth,
|
|
1295
|
+
frame: { x: m.x, y: m.y, width: m.width, height: m.height },
|
|
1296
|
+
originalIndex: i
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
if (info.parentIndex !== undefined) element.parentIndex = info.parentIndex;
|
|
1300
|
+
if (layout) element.layout = layout;
|
|
1301
|
+
if (textContent) element.text = textContent.slice(0, 100);
|
|
1302
|
+
|
|
1303
|
+
// Identifiers
|
|
1304
|
+
if (fiber.memoizedProps) {
|
|
1305
|
+
var identifiers = {};
|
|
1306
|
+
if (fiber.memoizedProps.testID) identifiers.testID = fiber.memoizedProps.testID;
|
|
1307
|
+
if (fiber.memoizedProps.accessibilityLabel) identifiers.accessibilityLabel = fiber.memoizedProps.accessibilityLabel;
|
|
1308
|
+
if (fiber.memoizedProps.nativeID) identifiers.nativeID = fiber.memoizedProps.nativeID;
|
|
1309
|
+
if (fiber.key) identifiers.key = fiber.key;
|
|
1310
|
+
if (Object.keys(identifiers).length > 0) element.identifiers = identifiers;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
elements.push(element);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// In componentsOnly mode, remove full-screen wrapper components
|
|
1317
|
+
// and re-parent their children to collapse the wrapper chain
|
|
1318
|
+
if (componentsOnly && elements.length > 0) {
|
|
1319
|
+
var filtered = [];
|
|
1320
|
+
// Map: originalIndex -> resolved parent for skipped wrappers
|
|
1321
|
+
var reparent = {};
|
|
1322
|
+
|
|
1323
|
+
for (var fi = 0; fi < elements.length; fi++) {
|
|
1324
|
+
var el = elements[fi];
|
|
1325
|
+
var fr = el.frame;
|
|
1326
|
+
// A wrapper is full-screen if it covers the entire viewport or extends beyond it
|
|
1327
|
+
// (e.g., y=-119 wrappers that extend behind the safe area)
|
|
1328
|
+
var isFullScreen = fr && fr.x <= 0 && fr.y <= 0 &&
|
|
1329
|
+
(fr.width >= viewportW - 2) && (fr.y + fr.height >= viewportH - 2);
|
|
1330
|
+
|
|
1331
|
+
if (isFullScreen) {
|
|
1332
|
+
// Skip this wrapper, map its originalIndex to its parent
|
|
1333
|
+
reparent[el.originalIndex] = el.parentIndex;
|
|
1334
|
+
} else {
|
|
1335
|
+
// Resolve parent through any skipped wrappers
|
|
1336
|
+
var resolvedParent = el.parentIndex;
|
|
1337
|
+
while (resolvedParent !== undefined && resolvedParent !== -1 && reparent[resolvedParent] !== undefined) {
|
|
1338
|
+
resolvedParent = reparent[resolvedParent];
|
|
1339
|
+
}
|
|
1340
|
+
el.parentIndex = resolvedParent;
|
|
1341
|
+
filtered.push(el);
|
|
1342
|
+
}
|
|
925
1343
|
}
|
|
1344
|
+
elements = filtered;
|
|
926
1345
|
}
|
|
927
1346
|
|
|
928
|
-
walkFiber(roots[0].current, 0, []);
|
|
929
|
-
|
|
930
|
-
// Summary mode: return counts by component name
|
|
931
1347
|
if (summaryMode) {
|
|
932
|
-
|
|
933
|
-
for (
|
|
934
|
-
counts[
|
|
1348
|
+
var counts = {};
|
|
1349
|
+
for (var j = 0; j < elements.length; j++) {
|
|
1350
|
+
counts[elements[j].component] = (counts[elements[j].component] || 0) + 1;
|
|
935
1351
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
.map(([name, count]) => ({ component: name, count }));
|
|
1352
|
+
var sorted = Object.keys(counts).map(function(name) {
|
|
1353
|
+
return { component: name, count: counts[name] };
|
|
1354
|
+
}).sort(function(a, b) { return b.count - a.count; });
|
|
940
1355
|
return {
|
|
941
1356
|
totalElements: elements.length,
|
|
942
1357
|
uniqueComponents: sorted.length,
|
|
@@ -945,15 +1360,15 @@ export async function getScreenLayout(options = {}) {
|
|
|
945
1360
|
}
|
|
946
1361
|
|
|
947
1362
|
return {
|
|
1363
|
+
viewport: { width: viewportW, height: viewportH },
|
|
948
1364
|
totalElements: elements.length,
|
|
949
1365
|
elements: elements
|
|
950
1366
|
};
|
|
951
1367
|
})()
|
|
952
1368
|
`;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (format === "tonl" && result.success && result.result) {
|
|
1369
|
+
const result = await executeInApp(resolveExpression, false, { timeoutMs: 30000 }, device);
|
|
1370
|
+
// Format output as tree
|
|
1371
|
+
if (result.success && result.result) {
|
|
957
1372
|
try {
|
|
958
1373
|
const parsed = JSON.parse(result.result);
|
|
959
1374
|
if (parsed.components) {
|
|
@@ -962,9 +1377,16 @@ export async function getScreenLayout(options = {}) {
|
|
|
962
1377
|
return { success: true, result: tonl };
|
|
963
1378
|
}
|
|
964
1379
|
else if (parsed.elements) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1380
|
+
if (raw) {
|
|
1381
|
+
return {
|
|
1382
|
+
success: true,
|
|
1383
|
+
result: result.result,
|
|
1384
|
+
parsedElements: parsed.elements,
|
|
1385
|
+
viewport: parsed.viewport
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const tree = formatScreenLayoutTree(parsed.elements, extended);
|
|
1389
|
+
return { success: true, result: tree };
|
|
968
1390
|
}
|
|
969
1391
|
}
|
|
970
1392
|
catch {
|
|
@@ -973,6 +1395,71 @@ export async function getScreenLayout(options = {}) {
|
|
|
973
1395
|
}
|
|
974
1396
|
return result;
|
|
975
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Format enriched elements as an indented tree with tap coordinates in pixels.
|
|
1400
|
+
* Same tree structure as get_screen_layout but with tap:(x,y) per node.
|
|
1401
|
+
*/
|
|
1402
|
+
function formatEnrichedLayoutTree(elements) {
|
|
1403
|
+
return formatLayoutTree(elements, (el, indent, isLeaf) => {
|
|
1404
|
+
const prefix = " ".repeat(indent);
|
|
1405
|
+
const frame = `(${Math.round(el.frame.x)},${Math.round(el.frame.y)} ${Math.round(el.frame.width)}x${Math.round(el.frame.height)})`;
|
|
1406
|
+
const tap = ` tap:(${el.tapX},${el.tapY})`;
|
|
1407
|
+
const id = el.identifiers?.testID || el.identifiers?.accessibilityLabel || "";
|
|
1408
|
+
const idStr = id ? ` [${id}]` : "";
|
|
1409
|
+
const textStr = el.text && isLeaf ? ` "${el.text}"` : "";
|
|
1410
|
+
return `${prefix}${el.component} ${frame}${tap}${idStr}${textStr}`;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Enrich screen layout data with tap-ready pixel coordinates for bundling with screenshots.
|
|
1415
|
+
* Converts points/dp frame coordinates to pixels using the device pixel ratio,
|
|
1416
|
+
* computes center-point tapX/tapY for each element.
|
|
1417
|
+
*
|
|
1418
|
+
* @param pixelRatio - device pixel ratio (e.g., 3 for @3x iPhone)
|
|
1419
|
+
* @param screenshotScaleFactor - if the screenshot image was scaled down, this factor adjusts coordinates
|
|
1420
|
+
* @param device - optional target device name
|
|
1421
|
+
* @returns formatted tree string with pixel coordinates, or null if unavailable
|
|
1422
|
+
*/
|
|
1423
|
+
export async function enrichScreenshotWithLayout(pixelRatio, screenshotScaleFactor, device) {
|
|
1424
|
+
try {
|
|
1425
|
+
const result = await getScreenLayout({ extended: false, summary: false, device, raw: true });
|
|
1426
|
+
if (!result.success || !result.parsedElements || result.parsedElements.length === 0)
|
|
1427
|
+
return null;
|
|
1428
|
+
const elements = result.parsedElements.map((el) => {
|
|
1429
|
+
const frame = el.frame || { x: 0, y: 0, width: 0, height: 0 };
|
|
1430
|
+
// Convert center point from points/dp to screenshot pixels
|
|
1431
|
+
// Points -> device pixels: multiply by pixelRatio
|
|
1432
|
+
// Device pixels -> screenshot pixels: divide by screenshotScaleFactor (if image was resized)
|
|
1433
|
+
const centerXPoints = frame.x + frame.width / 2;
|
|
1434
|
+
const centerYPoints = frame.y + frame.height / 2;
|
|
1435
|
+
const tapX = Math.round((centerXPoints * pixelRatio) / screenshotScaleFactor);
|
|
1436
|
+
const tapY = Math.round((centerYPoints * pixelRatio) / screenshotScaleFactor);
|
|
1437
|
+
// Convert frame to pixels too
|
|
1438
|
+
const pixelFrame = {
|
|
1439
|
+
x: Math.round((frame.x * pixelRatio) / screenshotScaleFactor),
|
|
1440
|
+
y: Math.round((frame.y * pixelRatio) / screenshotScaleFactor),
|
|
1441
|
+
width: Math.round((frame.width * pixelRatio) / screenshotScaleFactor),
|
|
1442
|
+
height: Math.round((frame.height * pixelRatio) / screenshotScaleFactor),
|
|
1443
|
+
};
|
|
1444
|
+
return {
|
|
1445
|
+
component: el.component,
|
|
1446
|
+
frame: pixelFrame,
|
|
1447
|
+
tapX,
|
|
1448
|
+
tapY,
|
|
1449
|
+
text: el.text,
|
|
1450
|
+
identifiers: el.identifiers,
|
|
1451
|
+
parentIndex: el.parentIndex,
|
|
1452
|
+
originalIndex: el.originalIndex,
|
|
1453
|
+
depth: el.depth,
|
|
1454
|
+
path: el.path,
|
|
1455
|
+
};
|
|
1456
|
+
});
|
|
1457
|
+
return formatEnrichedLayoutTree(elements);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
return null; // Non-fatal: screenshot works without layout
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
976
1463
|
/**
|
|
977
1464
|
* Inspect a specific component by name, returning its props, state, and layout.
|
|
978
1465
|
*/
|
|
@@ -1280,8 +1767,6 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1280
1767
|
child = child.sibling;
|
|
1281
1768
|
}
|
|
1282
1769
|
} catch(e) {
|
|
1283
|
-
// Skip nodes that throw (e.g. Reanimated animated components)
|
|
1284
|
-
// Still try to traverse children if possible
|
|
1285
1770
|
try {
|
|
1286
1771
|
var child = fiber.child;
|
|
1287
1772
|
while (child && results.length < maxResults) {
|
|
@@ -1294,7 +1779,6 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1294
1779
|
|
|
1295
1780
|
search(roots[0].current, [], 0);
|
|
1296
1781
|
|
|
1297
|
-
// Summary mode: just return counts by component name
|
|
1298
1782
|
if (summaryMode) {
|
|
1299
1783
|
const counts = {};
|
|
1300
1784
|
for (const r of results) {
|
|
@@ -1319,18 +1803,15 @@ export async function findComponents(pattern, options = {}) {
|
|
|
1319
1803
|
})()
|
|
1320
1804
|
`;
|
|
1321
1805
|
const result = await executeInApp(expression, false, {}, device);
|
|
1322
|
-
// Apply TONL formatting if requested
|
|
1323
1806
|
if (format === "tonl" && result.success && result.result) {
|
|
1324
1807
|
try {
|
|
1325
1808
|
const parsed = JSON.parse(result.result);
|
|
1326
1809
|
if (parsed.components) {
|
|
1327
1810
|
if (parsed.totalMatches !== undefined) {
|
|
1328
|
-
// Summary mode
|
|
1329
1811
|
const tonl = formatSummaryToTonl(parsed.components, parsed.totalMatches);
|
|
1330
1812
|
return { success: true, result: `pattern: ${parsed.pattern}\n${tonl}` };
|
|
1331
1813
|
}
|
|
1332
1814
|
else {
|
|
1333
|
-
// Full list mode
|
|
1334
1815
|
const tonl = formatFoundComponentsToTonl(parsed.components);
|
|
1335
1816
|
return { success: true, result: `pattern: ${parsed.pattern}\nfound: ${parsed.found}\n${tonl}` };
|
|
1336
1817
|
}
|
|
@@ -1387,7 +1868,7 @@ export async function pressElement(options) {
|
|
|
1387
1868
|
}
|
|
1388
1869
|
|
|
1389
1870
|
function extractText(fiber, depth) {
|
|
1390
|
-
if (!fiber || depth >
|
|
1871
|
+
if (!fiber || depth > 5000) return '';
|
|
1391
1872
|
var parts = [];
|
|
1392
1873
|
var props = fiber.memoizedProps;
|
|
1393
1874
|
if (props) {
|
|
@@ -1656,7 +2137,7 @@ export async function pressElement(options) {
|
|
|
1656
2137
|
var hostFiber = null;
|
|
1657
2138
|
var fallbackHost = null;
|
|
1658
2139
|
function findHostChild(fiber, depth) {
|
|
1659
|
-
if (!fiber || depth >
|
|
2140
|
+
if (!fiber || depth > 5000 || hostFiber) return;
|
|
1660
2141
|
if (fiber.tag === 5 && fiber.stateNode) {
|
|
1661
2142
|
var hostType = typeof fiber.type === 'string' ? fiber.type : '';
|
|
1662
2143
|
if (target.isInput && (hostType.indexOf('TextInput') !== -1 || hostType.indexOf('textinput') !== -1)) {
|
|
@@ -1806,7 +2287,7 @@ export async function isInspectorActive(device) {
|
|
|
1806
2287
|
if (roots.length === 0) return false;
|
|
1807
2288
|
|
|
1808
2289
|
function findComponent(fiber, targetName, depth = 0) {
|
|
1809
|
-
if (!fiber || depth >
|
|
2290
|
+
if (!fiber || depth > 5000) return null;
|
|
1810
2291
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1811
2292
|
if (name === targetName) return fiber;
|
|
1812
2293
|
let child = fiber.child;
|
|
@@ -1848,28 +2329,29 @@ export async function getInspectorSelection(device) {
|
|
|
1848
2329
|
}
|
|
1849
2330
|
if (roots.length === 0) return { error: 'No fiber roots found.' };
|
|
1850
2331
|
|
|
1851
|
-
// Find InspectorPanel
|
|
1852
|
-
function
|
|
1853
|
-
if (!fiber || depth >
|
|
2332
|
+
// Find all InspectorPanel instances (apps with modals may have multiple)
|
|
2333
|
+
function findAllPanels(fiber, targetName, depth, results) {
|
|
2334
|
+
if (!fiber || depth > 5000) return;
|
|
1854
2335
|
const name = fiber.type?.displayName || fiber.type?.name;
|
|
1855
|
-
if (name === targetName)
|
|
2336
|
+
if (name === targetName) results.push(fiber);
|
|
1856
2337
|
let child = fiber.child;
|
|
1857
2338
|
while (child) {
|
|
1858
|
-
|
|
1859
|
-
if (found) return found;
|
|
2339
|
+
findAllPanels(child, targetName, depth + 1, results);
|
|
1860
2340
|
child = child.sibling;
|
|
1861
2341
|
}
|
|
1862
|
-
return null;
|
|
1863
2342
|
}
|
|
1864
2343
|
|
|
1865
|
-
const
|
|
1866
|
-
|
|
2344
|
+
const panels = [];
|
|
2345
|
+
findAllPanels(roots[0].current, 'InspectorPanel', 0, panels);
|
|
2346
|
+
if (panels.length === 0) {
|
|
1867
2347
|
return {
|
|
1868
2348
|
error: 'Element Inspector is not active.',
|
|
1869
2349
|
hint: 'Use toggle_element_inspector to enable the inspector, then tap an element to select it.'
|
|
1870
2350
|
};
|
|
1871
2351
|
}
|
|
1872
2352
|
|
|
2353
|
+
// Prefer the panel that has an active selection
|
|
2354
|
+
const panelFiber = panels.find(p => p.memoizedProps.hierarchy?.length > 0) || panels[0];
|
|
1873
2355
|
const props = panelFiber.memoizedProps;
|
|
1874
2356
|
if (!props.hierarchy || props.hierarchy.length === 0) {
|
|
1875
2357
|
return {
|
|
@@ -2028,7 +2510,7 @@ export async function inspectAtPoint(x, y, options = {}) {
|
|
|
2028
2510
|
|
|
2029
2511
|
// RN primitives and internal components to skip when surfacing the "element" name.
|
|
2030
2512
|
// We want the nearest *custom* component, not a library wrapper.
|
|
2031
|
-
var RN_PRIMITIVES = /^(View|Text|Image|ScrollView|FlatList|SectionList|TextInput|TouchableOpacity|TouchableHighlight|TouchableNativeFeedback|TouchableWithoutFeedback|Pressable|Button|Switch|ActivityIndicator|
|
|
2513
|
+
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.*)$/;
|
|
2032
2514
|
|
|
2033
2515
|
function getNearestNamed(fiber, skipPrimitives) {
|
|
2034
2516
|
var cur = fiber;
|