main
js 2,430 lines 68.3 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 'use strict';
9
10 let installFacade;
11 let createTools;
12 let facade;
13 let React;
14 let ReactDOMClient;
15 let act;
16 let container;
17
18 // Profiler durations are timing-dependent: null when the build does not collect
19 // them, otherwise a non-negative number.
20 function isDuration(value) {
21 return value === null || (typeof value === 'number' && value >= 0);
22 }
23
24 describe('react-devtools-facade', () => {
25 beforeEach(() => {
26 jest.resetModules();
27 global.IS_REACT_ACT_ENVIRONMENT = true;
28
29 // The hook lives on globalThis, which jsdom shares across tests in this
30 // file, so a leftover hook would make installFacade() below throw. Remove
31 // it for a clean slate. (The facade never installs any other global, which
32 // the "does not install any tool globals" test verifies.)
33 delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
34
35 // Install the facade BEFORE React so the hook captures the first commit.
36 // Import through the package entry point to exercise the public surface.
37 const facadeAPI = require('../../index');
38 installFacade = facadeAPI.installFacade;
39 createTools = facadeAPI.createTools;
40 facade = installFacade();
41
42 React = require('react');
43 ReactDOMClient = require('react-dom/client');
44 act = React.act;
45
46 container = document.createElement('div');
47 });
48
49 afterEach(() => {
50 jest.dontMock('react-debug-tools');
51 container = null;
52 });
53
54 it('installs __REACT_DEVTOOLS_GLOBAL_HOOK__ on globalThis', () => {
55 expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
56 });
57
58 it('returns a Facade handle exposing the hook and tracked state', () => {
59 expect(facade.hook).toBe(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__);
60 expect(facade.fiberRoots).toBeInstanceOf(Map);
61 expect(facade.rendererInternals).toBeInstanceOf(Map);
62 expect(facade.profilingState).toEqual({
63 isActive: false,
64 currentTraceName: null,
65 traces: expect.any(Map),
66 onCommit: null,
67 onPostCommit: null,
68 });
69 });
70
71 it('does not install any tool globals (the integrator decides those)', () => {
72 expect(globalThis.__REACT_TOOLS__).toBeUndefined();
73 expect(globalThis.__REACT_LLM_TOOLS__).toBeUndefined();
74 });
75
76 it('attaches to an existing hook instead of installing a second one', () => {
77 // A facade hook is already installed on globalThis (beforeEach). A second
78 // installFacade() attaches to it rather than throwing or replacing it — this
79 // is the path taken when the React DevTools extension is present.
80 const attached = installFacade();
81 expect(attached.hook).toBe(facade.hook);
82 expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
83 });
84
85 it('an attached facade back-fills roots already tracked by the hook', () => {
86 function App() {
87 return <div>hi</div>;
88 }
89 act(() => {
90 ReactDOMClient.createRoot(container).render(<App />);
91 });
92
93 // Attaching after the app mounted picks up the already-tracked root.
94 const attached = installFacade();
95 const tree = createTools(attached).getComponentTree();
96 expect(tree.find(n => n.name === 'App')).toBeDefined();
97 });
98
99 it('an attached facade tracks later commits and profiles them', () => {
100 function Counter({count}) {
101 return <div>{'n:' + count}</div>;
102 }
103 const root = ReactDOMClient.createRoot(container);
104 act(() => {
105 root.render(<Counter count={0} />);
106 });
107
108 const tools = createTools(installFacade());
109 expect(
110 tools.getComponentTree().find(n => n.name === 'Counter'),
111 ).toBeDefined();
112
113 // A commit after attaching flows through the wrapped onCommitFiberRoot.
114 tools.startProfiling('attached-trace');
115 act(() => {
116 root.render(<Counter count={1} />);
117 });
118 expect(tools.stopProfiling()).toEqual({
119 status: 'stopped',
120 traceName: 'attached-trace',
121 commits: 1,
122 });
123 });
124
125 it('installs onto an explicit target without touching globalThis', () => {
126 const target = {};
127 const localFacade = installFacade(target);
128
129 expect(target.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(localFacade.hook);
130 // The explicit-target facade is fully independent of the global one.
131 expect(localFacade.hook).not.toBe(facade.hook);
132 expect(localFacade.fiberRoots).not.toBe(facade.fiberRoots);
133 // ...and installing onto a target does not disturb the global hook.
134 expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
135 });
136
137 it('records the renderer and its fiber root on mount', () => {
138 function Greeting() {
139 return <div>Hello</div>;
140 }
141
142 act(() => {
143 ReactDOMClient.createRoot(container).render(<Greeting />);
144 });
145
146 // React injected a renderer: its internal constants were captured...
147 expect(facade.rendererInternals.size).toBeGreaterThan(0);
148 // ...and the hook recorded the committed root in facade.fiberRoots.
149 let totalRoots = 0;
150 facade.fiberRoots.forEach(roots => {
151 totalRoots += roots.size;
152 });
153 expect(totalRoots).toBeGreaterThan(0);
154 });
155
156 it('removes unmounted roots from tracking', () => {
157 function App() {
158 return <div>hello</div>;
159 }
160
161 const root = ReactDOMClient.createRoot(container);
162 act(() => {
163 root.render(<App />);
164 });
165
166 const rendererID = Array.from(facade.hook.renderers.keys())[0];
167 expect(facade.hook.getFiberRoots(rendererID).size).toBeGreaterThan(0);
168
169 act(() => {
170 root.unmount();
171 });
172
173 expect(facade.hook.getFiberRoots(rendererID).size).toBe(0);
174 });
175
176 describe('getComponentTree', () => {
177 let getComponentTree;
178
179 beforeEach(() => {
180 getComponentTree = createTools(facade).getComponentTree;
181 });
182
183 it('returns error when nothing is rendered', () => {
184 const result = getComponentTree();
185 expect(result.error).toMatch(/No mounted React roots found/);
186 });
187
188 it('returns an array of component nodes', () => {
189 function App() {
190 return <div>hello</div>;
191 }
192
193 act(() => {
194 ReactDOMClient.createRoot(container).render(<App />);
195 });
196
197 const result = getComponentTree();
198 expect(Array.isArray(result)).toBe(true);
199 const app = result.find(n => n.name === 'App');
200 const div = result.find(n => n.name === 'div');
201 // App is the root's only child; its child is the host div.
202 expect(app).toEqual({
203 uid: 'r0',
204 type: 'function',
205 name: 'App',
206 key: null,
207 firstChild: div.uid,
208 nextSibling: null,
209 });
210 // A single string child ('hello') is stored as a prop, not a child fiber,
211 // so the div is a leaf in the tree.
212 expect(div).toEqual({
213 uid: 'r2',
214 type: 'host',
215 name: 'div',
216 key: null,
217 firstChild: null,
218 nextSibling: null,
219 });
220 });
221
222 it('encodes firstChild and nextSibling relationships', () => {
223 function Header() {
224 return <h1>title</h1>;
225 }
226 function Footer() {
227 return <footer>foot</footer>;
228 }
229 function App() {
230 return (
231 <div>
232 <Header />
233 <Footer />
234 </div>
235 );
236 }
237
238 act(() => {
239 ReactDOMClient.createRoot(container).render(<App />);
240 });
241
242 const nodes = getComponentTree();
243 const app = nodes.find(n => n.name === 'App');
244 const div = nodes.find(n => n.name === 'div');
245 const header = nodes.find(n => n.name === 'Header');
246 const footer = nodes.find(n => n.name === 'Footer');
247
248 // App's firstChild is div
249 expect(app.firstChild).toBe(div.uid);
250 // div's firstChild is Header
251 expect(div.firstChild).toBe(header.uid);
252 // Header's nextSibling is Footer
253 expect(header.nextSibling).toBe(footer.uid);
254 // Footer has no nextSibling
255 expect(footer.nextSibling).toBe(null);
256 });
257
258 it('shows keys in the output', () => {
259 function Item() {
260 return <li>item</li>;
261 }
262 function List() {
263 return (
264 <ul>
265 <Item key="a" />
266 <Item key="b" />
267 </ul>
268 );
269 }
270
271 act(() => {
272 ReactDOMClient.createRoot(container).render(<List />);
273 });
274
275 const items = getComponentTree().filter(n => n.name === 'Item');
276 expect(items.map(i => i.key)).toEqual(['a', 'b']);
277 });
278
279 it('limits depth with the depth parameter', () => {
280 function Child() {
281 return <span>leaf</span>;
282 }
283 function Parent() {
284 return <Child />;
285 }
286 function App() {
287 return <Parent />;
288 }
289
290 act(() => {
291 ReactDOMClient.createRoot(container).render(<App />);
292 });
293
294 const names = snapshot => snapshot.map(n => n.name);
295
296 // depth=0: only the root node (HostRoot)
297 const shallow = getComponentTree(0);
298 expect(shallow).toHaveLength(1);
299 expect(shallow[0].type).toBe('root');
300
301 // depth=1: root + App
302 const d1 = getComponentTree(1);
303 expect(names(d1)).toContain('App');
304 expect(names(d1)).not.toContain('Parent');
305
306 // depth=2: root + App + Parent
307 const d2 = getComponentTree(2);
308 expect(names(d2)).toContain('App');
309 expect(names(d2)).toContain('Parent');
310 expect(names(d2)).not.toContain('Child');
311
312 const deep = getComponentTree(20);
313 expect(names(deep)).toEqual(
314 expect.arrayContaining(['App', 'Parent', 'Child']),
315 );
316 });
317
318 it('starts from a specific node when rootUid is provided', () => {
319 function Nav() {
320 return <nav>nav</nav>;
321 }
322 function Header() {
323 return <Nav />;
324 }
325 function Footer() {
326 return <footer>foot</footer>;
327 }
328 function App() {
329 return (
330 <div>
331 <Header />
332 <Footer />
333 </div>
334 );
335 }
336
337 act(() => {
338 ReactDOMClient.createRoot(container).render(<App />);
339 });
340
341 // First, get the full tree to find Header's uid
342 const header = getComponentTree().find(n => n.name === 'Header');
343 expect(header).toBeDefined();
344
345 // Snapshot from Header
346 const sub = getComponentTree(20, header.uid);
347 const names = sub.map(n => n.name);
348 expect(names).toContain('Header');
349 expect(names).toContain('Nav');
350 // Should NOT contain App or Footer
351 expect(names).not.toContain('App');
352 expect(names).not.toContain('Footer');
353 });
354
355 it('returns error for non-existent rootUid', () => {
356 function App() {
357 return <div>hello</div>;
358 }
359
360 act(() => {
361 ReactDOMClient.createRoot(container).render(<App />);
362 });
363
364 const result = getComponentTree(20, 'r9999');
365 expect(result.error).toMatch(/Component not found/);
366 });
367
368 it('assigns stable uids across calls', () => {
369 function App() {
370 return <div>hello</div>;
371 }
372
373 act(() => {
374 ReactDOMClient.createRoot(container).render(<App />);
375 });
376
377 const first = getComponentTree();
378 const second = getComponentTree();
379 expect(first).toEqual(second);
380 });
381
382 it('shows class components with class type', () => {
383 class MyComponent extends React.Component {
384 render() {
385 return <div>class</div>;
386 }
387 }
388
389 act(() => {
390 ReactDOMClient.createRoot(container).render(<MyComponent />);
391 });
392
393 const node = getComponentTree().find(n => n.name === 'MyComponent');
394 expect(node.type).toBe('class');
395 });
396
397 it('shows host components with host type', () => {
398 function App() {
399 return <div>hello</div>;
400 }
401
402 act(() => {
403 ReactDOMClient.createRoot(container).render(<App />);
404 });
405
406 const node = getComponentTree().find(n => n.name === 'div');
407 expect(node.type).toBe('host');
408 });
409
410 it('shows Memo components with memo type', () => {
411 function Inner() {
412 return <span>inner</span>;
413 }
414 const Memoized = React.memo(Inner);
415
416 act(() => {
417 ReactDOMClient.createRoot(container).render(<Memoized />);
418 });
419
420 const node = getComponentTree().find(n => n.name === 'Memo(Inner)');
421 expect(node).toBeDefined();
422 expect(node.type).toBe('memo');
423 });
424
425 it('shows ForwardRef components with forwardRef type', () => {
426 const FancyButton = React.forwardRef(function FancyButton(props, ref) {
427 return <button ref={ref}>{props.children}</button>;
428 });
429
430 act(() => {
431 ReactDOMClient.createRoot(container).render(
432 <FancyButton>click</FancyButton>,
433 );
434 });
435
436 const node = getComponentTree().find(
437 n => n.name === 'ForwardRef(FancyButton)',
438 );
439 expect(node).toBeDefined();
440 expect(node.type).toBe('forwardRef');
441 });
442
443 it('includes Fragment in the tree', () => {
444 function A() {
445 return <span>a</span>;
446 }
447 function B() {
448 return <span>b</span>;
449 }
450 function App() {
451 // Keyed Fragment creates a Fragment fiber
452 return (
453 <div>
454 <React.Fragment key="group">
455 <A />
456 <B />
457 </React.Fragment>
458 </div>
459 );
460 }
461
462 act(() => {
463 ReactDOMClient.createRoot(container).render(<App />);
464 });
465
466 const nodes = getComponentTree();
467 const fragment = nodes.find(n => n.type === 'fragment');
468 const a = nodes.find(n => n.name === 'A');
469 const b = nodes.find(n => n.name === 'B');
470 // The keyed Fragment is the div's child and parents A then B.
471 expect(fragment).toEqual({
472 uid: 'r3',
473 type: 'fragment',
474 name: 'Fragment',
475 key: 'group',
476 firstChild: a.uid,
477 nextSibling: null,
478 });
479 expect(a).toEqual({
480 uid: 'r4',
481 type: 'function',
482 name: 'A',
483 key: null,
484 firstChild: 'r5',
485 nextSibling: b.uid,
486 });
487 expect(b).toEqual({
488 uid: 'r6',
489 type: 'function',
490 name: 'B',
491 key: null,
492 firstChild: 'r7',
493 nextSibling: null,
494 });
495 });
496
497 it('includes HostRoot with type root', () => {
498 function App() {
499 return <div>hello</div>;
500 }
501
502 act(() => {
503 ReactDOMClient.createRoot(container).render(<App />);
504 });
505
506 const nodes = getComponentTree();
507 const root = nodes.find(n => n.type === 'root');
508 const app = nodes.find(n => n.name === 'App');
509 // The HostRoot is the tree's entry; its only child is App.
510 expect(root).toEqual({
511 uid: 'r1',
512 type: 'root',
513 name: 'createRoot()',
514 key: null,
515 firstChild: app.uid,
516 nextSibling: null,
517 });
518 });
519
520 it('includes Suspense in the tree', () => {
521 function App() {
522 return (
523 <React.Suspense fallback={<div>loading</div>}>
524 <div>content</div>
525 </React.Suspense>
526 );
527 }
528
529 act(() => {
530 ReactDOMClient.createRoot(container).render(<App />);
531 });
532
533 const suspense = getComponentTree().find(n => n.type === 'suspense');
534 // Suspense wraps its content via an internal primary/Offscreen child, so
535 // firstChild is a valid uid but its exact identity is an internal detail.
536 expect(suspense).toEqual({
537 uid: 'r2',
538 type: 'suspense',
539 name: 'Suspense',
540 key: null,
541 firstChild: 'r3',
542 nextSibling: null,
543 });
544 });
545
546 it('includes Context Provider in the tree', () => {
547 const MyContext = React.createContext('default');
548 function App() {
549 return (
550 <MyContext value="test">
551 <div>child</div>
552 </MyContext>
553 );
554 }
555
556 act(() => {
557 ReactDOMClient.createRoot(container).render(<App />);
558 });
559
560 const provider = getComponentTree().find(n => n.type === 'context');
561 expect(provider).toEqual({
562 uid: 'r2',
563 type: 'context',
564 name: 'Context.Provider',
565 key: null,
566 firstChild: 'r3',
567 nextSibling: null,
568 });
569 });
570
571 it('uids survive re-renders via alternate fiber handling', () => {
572 function Counter({count}) {
573 return <div>{'Count: ' + count}</div>;
574 }
575
576 const root = ReactDOMClient.createRoot(container);
577 act(() => {
578 root.render(<Counter count={0} />);
579 });
580
581 const counter1 = getComponentTree().find(n => n.name === 'Counter');
582 expect(counter1).toBeDefined();
583
584 act(() => {
585 root.render(<Counter count={1} />);
586 });
587
588 const counter2 = getComponentTree().find(n => n.name === 'Counter');
589 expect(counter2).toBeDefined();
590 // Same uid after re-render
591 expect(counter2.uid).toBe(counter1.uid);
592 });
593
594 it('removes unmounted roots from the tree', () => {
595 function App() {
596 return <div>hello</div>;
597 }
598
599 const root = ReactDOMClient.createRoot(container);
600 act(() => {
601 root.render(<App />);
602 });
603
604 const before = getComponentTree();
605 expect(before.find(n => n.name === 'App')).toBeDefined();
606
607 act(() => {
608 root.unmount();
609 });
610
611 const after = getComponentTree();
612 expect(after.error).toMatch(/No mounted React roots found/);
613 });
614 });
615
616 describe('findComponents', () => {
617 let findComponents;
618 let getComponentTree;
619
620 beforeEach(() => {
621 const tools = createTools(facade);
622 findComponents = tools.findComponents;
623 getComponentTree = tools.getComponentTree;
624 });
625
626 it('finds components by name (case-insensitive substring match)', () => {
627 function Header() {
628 return <h1>title</h1>;
629 }
630 function Footer() {
631 return <footer>foot</footer>;
632 }
633 function App() {
634 return (
635 <div>
636 <Header />
637 <Footer />
638 </div>
639 );
640 }
641
642 act(() => {
643 ReactDOMClient.createRoot(container).render(<App />);
644 });
645
646 const result = findComponents('header');
647 expect(result.totalCount).toBe(1);
648 expect(result.results).toHaveLength(1);
649 expect(result.results[0].name).toBe('Header');
650 expect(result.results[0].type).toBe('function');
651 expect(result.results[0].uid).toBe('r0');
652 });
653
654 it('returns all matches when multiple components match', () => {
655 function Card() {
656 return <div>card</div>;
657 }
658 function App() {
659 return (
660 <div>
661 <Card key="a" />
662 <Card key="b" />
663 <Card key="c" />
664 </div>
665 );
666 }
667
668 act(() => {
669 ReactDOMClient.createRoot(container).render(<App />);
670 });
671
672 const result = findComponents('Card');
673 expect(result.totalCount).toBe(3);
674 expect(result.results.map(r => r.key)).toEqual(['a', 'b', 'c']);
675 });
676
677 it('returns empty results when no components match', () => {
678 function App() {
679 return <div>hello</div>;
680 }
681
682 act(() => {
683 ReactDOMClient.createRoot(container).render(<App />);
684 });
685
686 const result = findComponents('NonExistent');
687 expect(result.totalCount).toBe(0);
688 expect(result.results).toEqual([]);
689 expect(result.page).toBe(1);
690 expect(result.totalPages).toBe(1);
691 });
692
693 it('scopes search to subtree when rootUid is provided', () => {
694 function Badge() {
695 return <span>badge</span>;
696 }
697 function Sidebar() {
698 return <Badge />;
699 }
700 function Main() {
701 return <Badge />;
702 }
703 function App() {
704 return (
705 <div>
706 <Sidebar />
707 <Main />
708 </div>
709 );
710 }
711
712 act(() => {
713 ReactDOMClient.createRoot(container).render(<App />);
714 });
715
716 // Find Sidebar's uid
717 const sidebar = getComponentTree().find(n => n.name === 'Sidebar');
718 expect(sidebar).toBeDefined();
719
720 // Search for Badge only under Sidebar
721 const result = findComponents('Badge', sidebar.uid);
722 expect(result.totalCount).toBe(1);
723 expect(result.results[0].name).toBe('Badge');
724
725 // Without rootUid, should find both Badges
726 const allResult = findComponents('Badge');
727 expect(allResult.totalCount).toBe(2);
728 });
729
730 it('paginates results with default page size of 10', () => {
731 function Item() {
732 return <li>item</li>;
733 }
734 function App() {
735 const items = [];
736 for (let i = 0; i < 15; i++) {
737 items.push(<Item key={String(i)} />);
738 }
739 return <ul>{items}</ul>;
740 }
741
742 act(() => {
743 ReactDOMClient.createRoot(container).render(<App />);
744 });
745
746 const page1 = findComponents('Item');
747 expect(page1.totalCount).toBe(15);
748 expect(page1.page).toBe(1);
749 expect(page1.pageSize).toBe(10);
750 expect(page1.totalPages).toBe(2);
751 expect(page1.results).toHaveLength(10);
752
753 const page2 = findComponents('Item', undefined, 2);
754 expect(page2.page).toBe(2);
755 expect(page2.results).toHaveLength(5);
756 });
757
758 it('supports custom page size', () => {
759 function Item() {
760 return <li>item</li>;
761 }
762 function App() {
763 return (
764 <ul>
765 <Item key="0" />
766 <Item key="1" />
767 <Item key="2" />
768 <Item key="3" />
769 <Item key="4" />
770 </ul>
771 );
772 }
773
774 act(() => {
775 ReactDOMClient.createRoot(container).render(<App />);
776 });
777
778 const result = findComponents('Item', undefined, 1, 2);
779 expect(result.totalCount).toBe(5);
780 expect(result.pageSize).toBe(2);
781 expect(result.totalPages).toBe(3);
782 expect(result.results).toHaveLength(2);
783 expect(result.results[0].key).toBe('0');
784 expect(result.results[1].key).toBe('1');
785
786 const page3 = findComponents('Item', undefined, 3, 2);
787 expect(page3.results).toHaveLength(1);
788 expect(page3.results[0].key).toBe('4');
789 });
790
791 it('clamps page number to valid range', () => {
792 function App() {
793 return <div>hello</div>;
794 }
795
796 act(() => {
797 ReactDOMClient.createRoot(container).render(<App />);
798 });
799
800 // Page 0 should clamp to 1
801 const low = findComponents('div', undefined, 0);
802 expect(low.page).toBe(1);
803
804 // Page beyond total should clamp to last page
805 const high = findComponents('div', undefined, 999);
806 expect(high.page).toBe(1);
807 });
808
809 it('results have same shape as tree snapshot nodes', () => {
810 function Widget() {
811 return <span>w</span>;
812 }
813 function App() {
814 return <Widget />;
815 }
816
817 act(() => {
818 ReactDOMClient.createRoot(container).render(<App />);
819 });
820
821 const result = findComponents('Widget');
822 expect(result.results).toHaveLength(1);
823 expect(result.results[0]).toEqual({
824 uid: 'r0',
825 type: 'function',
826 name: 'Widget',
827 key: null,
828 firstChild: 'r1',
829 nextSibling: null,
830 });
831 });
832
833 it('uids are consistent with getComponentTree', () => {
834 function Target() {
835 return <div>target</div>;
836 }
837 function App() {
838 return <Target />;
839 }
840
841 act(() => {
842 ReactDOMClient.createRoot(container).render(<App />);
843 });
844
845 // Get uid from tree snapshot
846 const target = getComponentTree().find(n => n.name === 'Target');
847 expect(target).toBeDefined();
848
849 // findComponents should return the same uid
850 const result = findComponents('Target');
851 expect(result.results[0].uid).toBe(target.uid);
852 });
853
854 it('matches host components by tag name', () => {
855 function App() {
856 return (
857 <div>
858 <span>a</span>
859 <span>b</span>
860 </div>
861 );
862 }
863
864 act(() => {
865 ReactDOMClient.createRoot(container).render(<App />);
866 });
867
868 const result = findComponents('span');
869 expect(result.totalCount).toBe(2);
870 expect(result.results[0].type).toBe('host');
871 expect(result.results[0].name).toBe('span');
872 });
873
874 it('does not match internal nodes with null displayName', () => {
875 function App() {
876 return (
877 <React.Fragment>
878 <div>hello</div>
879 </React.Fragment>
880 );
881 }
882
883 act(() => {
884 ReactDOMClient.createRoot(container).render(<App />);
885 });
886
887 // Fragment has null displayName in getDisplayNameForFiber,
888 // so it should not appear in search results
889 const fragmentResult = findComponents('Fragment');
890 expect(fragmentResult.totalCount).toBe(0);
891 });
892
893 it('finds Memo components by wrapped display name', () => {
894 function Inner() {
895 return <span>inner</span>;
896 }
897 const Memoized = React.memo(Inner);
898 function App() {
899 return <Memoized />;
900 }
901
902 act(() => {
903 ReactDOMClient.createRoot(container).render(<App />);
904 });
905
906 // memo(Inner) renders Inner inline (no separate FunctionComponent fiber),
907 // so the only match for "Inner" is the memo wrapper "Memo(Inner)".
908 const result = findComponents('Inner');
909 expect(result.totalCount).toBe(1);
910 expect(result.results).toHaveLength(1);
911 expect(result.results[0]).toEqual({
912 uid: 'r0',
913 type: 'memo',
914 name: 'Memo(Inner)',
915 key: null,
916 firstChild: 'r1',
917 nextSibling: null,
918 });
919 });
920
921 it('returns error for non-existent rootUid in scoped search', () => {
922 function App() {
923 return <div>hello</div>;
924 }
925
926 act(() => {
927 ReactDOMClient.createRoot(container).render(<App />);
928 });
929
930 const result = findComponents('App', 'r9999');
931 expect(result.error).toMatch(/Component not found/);
932 });
933 });
934
935 describe('getComponentSource', () => {
936 let getComponentSource;
937 let getComponentTree;
938
939 beforeEach(() => {
940 const tools = createTools(facade);
941 getComponentSource = tools.getComponentSource;
942 getComponentTree = tools.getComponentTree;
943 });
944
945 it('returns {source: null} for a function component when the location is unavailable', () => {
946 // The throwing trick that resolves a component's definition location does
947 // not produce file positions under jsdom, so source is null here. In a
948 // real browser this returns {name, fileName, line, column}.
949 function Greeting() {
950 return <div>Hello</div>;
951 }
952
953 act(() => {
954 ReactDOMClient.createRoot(container).render(<Greeting />);
955 });
956
957 const greeting = getComponentTree().find(n => n.name === 'Greeting');
958 expect(greeting).toBeDefined();
959 expect(getComponentSource(greeting.uid)).toEqual({source: null});
960 });
961
962 it('returns {source: null} for host components', () => {
963 function App() {
964 return <div>hello</div>;
965 }
966
967 act(() => {
968 ReactDOMClient.createRoot(container).render(<App />);
969 });
970
971 const div = getComponentTree().find(n => n.name === 'div');
972 expect(div).toBeDefined();
973 // Host components like div have no source location.
974 expect(getComponentSource(div.uid)).toEqual({source: null});
975 });
976
977 it('returns error for non-existent uid', () => {
978 const result = getComponentSource('r9999');
979 expect(result.error).toMatch(/Component not found/);
980 });
981 });
982
983 describe('getOwnerStackTrace', () => {
984 let getOwnerStackTrace;
985 let getComponentTree;
986
987 beforeEach(() => {
988 const tools = createTools(facade);
989 getOwnerStackTrace = tools.getOwnerStackTrace;
990 getComponentTree = tools.getComponentTree;
991 });
992
993 it('returns a stack string for a nested component', () => {
994 function Child() {
995 return <span>leaf</span>;
996 }
997 function Parent() {
998 return <Child />;
999 }
1000 function App() {
1001 return <Parent />;
1002 }
1003
1004 act(() => {
1005 ReactDOMClient.createRoot(container).render(<App />);
1006 });
1007
1008 const child = getComponentTree().find(n => n.name === 'Child');
1009 expect(child).toBeDefined();
1010
1011 const result = getOwnerStackTrace(child.uid);
1012 expect(typeof result.stack).toBe('string');
1013 // The stack should mention the owner components
1014 expect(result.stack).toContain('Parent');
1015 expect(result.stack).toContain('App');
1016 });
1017
1018 it('returns a stack string for the root component', () => {
1019 function App() {
1020 return <div>hello</div>;
1021 }
1022
1023 act(() => {
1024 ReactDOMClient.createRoot(container).render(<App />);
1025 });
1026
1027 const app = getComponentTree().find(n => n.name === 'App');
1028 const result = getOwnerStackTrace(app.uid);
1029 expect(typeof result.stack).toBe('string');
1030 });
1031
1032 it('returns error for non-existent uid', () => {
1033 const result = getOwnerStackTrace('r9999');
1034 expect(result.error).toMatch(/Component not found/);
1035 });
1036 });
1037
1038 describe('getParentStack', () => {
1039 let getParentStack;
1040 let getOwnerStack;
1041 let getComponentTree;
1042
1043 beforeEach(() => {
1044 const tools = createTools(facade);
1045 getParentStack = tools.getParentStack;
1046 getOwnerStack = tools.getOwnerStack;
1047 getComponentTree = tools.getComponentTree;
1048 });
1049
1050 it('returns structural parents from immediate parent to host root', () => {
1051 function Child() {
1052 return <span>leaf</span>;
1053 }
1054 function App() {
1055 return (
1056 <section>
1057 <Child />
1058 </section>
1059 );
1060 }
1061
1062 act(() => {
1063 ReactDOMClient.createRoot(container).render(<App />);
1064 });
1065
1066 const child = getComponentTree().find(n => n.name === 'Child');
1067 expect(child).toBeDefined();
1068
1069 const parents = getParentStack(child.uid);
1070 expect(parents).toEqual([
1071 {
1072 uid: 'r2',
1073 name: 'section',
1074 type: 'host',
1075 },
1076 {
1077 uid: 'r0',
1078 name: 'App',
1079 type: 'function',
1080 },
1081 {
1082 uid: 'r1',
1083 name: expect.any(String),
1084 type: 'root',
1085 },
1086 ]);
1087 });
1088
1089 it('distinguishes structural parents from JSX owners', () => {
1090 function Child() {
1091 return <span>leaf</span>;
1092 }
1093 function App() {
1094 return (
1095 <section>
1096 <Child />
1097 </section>
1098 );
1099 }
1100
1101 act(() => {
1102 ReactDOMClient.createRoot(container).render(<App />);
1103 });
1104
1105 const child = getComponentTree().find(n => n.name === 'Child');
1106 const parents = getParentStack(child.uid);
1107 const owners = getOwnerStack(child.uid);
1108
1109 expect(parents[0]).toMatchObject({
1110 name: 'section',
1111 type: 'host',
1112 });
1113 expect(owners[0]).toMatchObject({
1114 name: 'App',
1115 type: 'function',
1116 });
1117 });
1118
1119 it('returns an empty array for the host root', () => {
1120 function App() {
1121 return <div>hello</div>;
1122 }
1123
1124 act(() => {
1125 ReactDOMClient.createRoot(container).render(<App />);
1126 });
1127
1128 const root = getComponentTree().find(n => n.type === 'root');
1129 expect(getParentStack(root.uid)).toEqual([]);
1130 });
1131
1132 it('returns error for non-existent uid', () => {
1133 const result = getParentStack('r9999');
1134 expect(result.error).toMatch(/Component not found/);
1135 });
1136 });
1137
1138 describe('getOwnerStack', () => {
1139 let getOwnerStack;
1140 let getComponentTree;
1141
1142 beforeEach(() => {
1143 const tools = createTools(facade);
1144 getOwnerStack = tools.getOwnerStack;
1145 getComponentTree = tools.getComponentTree;
1146 });
1147
1148 it('returns owner list for a nested component', () => {
1149 function Child() {
1150 return <span>leaf</span>;
1151 }
1152 function Parent() {
1153 return <Child />;
1154 }
1155 function App() {
1156 return <Parent />;
1157 }
1158
1159 act(() => {
1160 ReactDOMClient.createRoot(container).render(<App />);
1161 });
1162
1163 const child = getComponentTree().find(n => n.name === 'Child');
1164 expect(child).toBeDefined();
1165
1166 const owners = getOwnerStack(child.uid);
1167 expect(owners).toEqual([
1168 {
1169 uid: 'r2',
1170 name: 'Parent',
1171 type: 'function',
1172 },
1173 {
1174 uid: 'r0',
1175 name: 'App',
1176 type: 'function',
1177 },
1178 ]);
1179 });
1180
1181 it('each entry has uid, name, and type', () => {
1182 function Child() {
1183 return <span>leaf</span>;
1184 }
1185 function App() {
1186 return <Child />;
1187 }
1188
1189 act(() => {
1190 ReactDOMClient.createRoot(container).render(<App />);
1191 });
1192
1193 const child = getComponentTree().find(n => n.name === 'Child');
1194 const owners = getOwnerStack(child.uid);
1195
1196 expect(owners).toHaveLength(1);
1197 expect(owners[0].uid).toBe('r0');
1198 expect(owners[0].name).toBe('App');
1199 expect(owners[0].type).toBe('function');
1200 });
1201
1202 it('owner uids are consistent with getComponentTree', () => {
1203 function Child() {
1204 return <span>leaf</span>;
1205 }
1206 function App() {
1207 return <Child />;
1208 }
1209
1210 act(() => {
1211 ReactDOMClient.createRoot(container).render(<App />);
1212 });
1213
1214 const tree = getComponentTree();
1215 const child = tree.find(n => n.name === 'Child');
1216 const app = tree.find(n => n.name === 'App');
1217
1218 const owners = getOwnerStack(child.uid);
1219 expect(owners[0].uid).toBe(app.uid);
1220 });
1221
1222 it('returns empty array for root component with no owner', () => {
1223 function App() {
1224 return <div>hello</div>;
1225 }
1226
1227 act(() => {
1228 ReactDOMClient.createRoot(container).render(<App />);
1229 });
1230
1231 const app = getComponentTree().find(n => n.name === 'App');
1232 const owners = getOwnerStack(app.uid);
1233 expect(owners).toEqual([]);
1234 });
1235
1236 it('returns error for non-existent uid', () => {
1237 const result = getOwnerStack('r9999');
1238 expect(result.error).toMatch(/Component not found/);
1239 });
1240
1241 it('is ordered from immediate owner to root ancestor', () => {
1242 function GrandChild() {
1243 return <span>gc</span>;
1244 }
1245 function Child() {
1246 return <GrandChild />;
1247 }
1248 function Parent() {
1249 return <Child />;
1250 }
1251 function App() {
1252 return <Parent />;
1253 }
1254
1255 act(() => {
1256 ReactDOMClient.createRoot(container).render(<App />);
1257 });
1258
1259 const gc = getComponentTree().find(n => n.name === 'GrandChild');
1260 const owners = getOwnerStack(gc.uid);
1261 expect(owners).toEqual([
1262 {
1263 uid: 'r3',
1264 name: 'Child',
1265 type: 'function',
1266 },
1267 {
1268 uid: 'r2',
1269 name: 'Parent',
1270 type: 'function',
1271 },
1272 {
1273 uid: 'r0',
1274 name: 'App',
1275 type: 'function',
1276 },
1277 ]);
1278 });
1279 });
1280
1281 describe('getComponentByUid', () => {
1282 let getComponentTree;
1283 let getComponentByUid;
1284
1285 beforeEach(() => {
1286 const tools = createTools(facade);
1287 getComponentTree = tools.getComponentTree;
1288 getComponentByUid = tools.getComponentByUid;
1289 });
1290
1291 it('returns error for non-existent uid', () => {
1292 const result = getComponentByUid('r9999');
1293 expect(result.error).toMatch(/Component not found/);
1294 });
1295
1296 it('returns info for a function component', () => {
1297 function Greeting() {
1298 return <div>Hello</div>;
1299 }
1300
1301 act(() => {
1302 ReactDOMClient.createRoot(container).render(<Greeting />);
1303 });
1304
1305 const greeting = getComponentTree().find(n => n.name === 'Greeting');
1306 expect(greeting).toBeDefined();
1307 const info = getComponentByUid(greeting.uid);
1308
1309 expect(info.uid).toBe(greeting.uid);
1310 expect(info.type).toBe('function');
1311 expect(info.name).toBe('Greeting');
1312 });
1313
1314 it('returns props (excluding children)', () => {
1315 function Button() {
1316 return <button>click</button>;
1317 }
1318
1319 act(() => {
1320 ReactDOMClient.createRoot(container).render(
1321 <Button text="Click me" disabled={true} />,
1322 );
1323 });
1324
1325 const button = getComponentTree().find(n => n.name === 'Button');
1326 const info = getComponentByUid(button.uid);
1327
1328 expect(info.props.text).toBe('Click me');
1329 expect(info.props.disabled).toBe(true);
1330 expect(info.props).not.toHaveProperty('children');
1331 });
1332
1333 it('serializes function props as descriptive strings', () => {
1334 function Button() {
1335 return <button>click</button>;
1336 }
1337
1338 function handleClick() {}
1339
1340 act(() => {
1341 ReactDOMClient.createRoot(container).render(
1342 <Button onClick={handleClick} />,
1343 );
1344 });
1345
1346 const button = getComponentTree().find(n => n.name === 'Button');
1347 const info = getComponentByUid(button.uid);
1348
1349 expect(info.props.onClick).toBe('[fn handleClick]');
1350 });
1351
1352 it('returns key when present', () => {
1353 function Item() {
1354 return <li>item</li>;
1355 }
1356 function List() {
1357 return (
1358 <ul>
1359 <Item key="first" />
1360 </ul>
1361 );
1362 }
1363
1364 act(() => {
1365 ReactDOMClient.createRoot(container).render(<List />);
1366 });
1367
1368 const item = getComponentTree().find(n => n.name === 'Item');
1369 const info = getComponentByUid(item.uid);
1370
1371 expect(info.key).toBe('first');
1372 });
1373
1374 it('returns correct type for class components', () => {
1375 class MyClass extends React.Component {
1376 render() {
1377 return <div>class</div>;
1378 }
1379 }
1380
1381 act(() => {
1382 ReactDOMClient.createRoot(container).render(<MyClass />);
1383 });
1384
1385 const myClass = getComponentTree().find(n => n.name === 'MyClass');
1386 expect(myClass).toBeDefined();
1387 const info = getComponentByUid(myClass.uid);
1388
1389 expect(info.type).toBe('class');
1390 expect(info.name).toBe('MyClass');
1391 });
1392
1393 it('returns correct type for host components', () => {
1394 function App() {
1395 return <div className="app" id="root" />;
1396 }
1397
1398 act(() => {
1399 ReactDOMClient.createRoot(container).render(<App />);
1400 });
1401
1402 const div = getComponentTree().find(n => n.name === 'div');
1403 const info = getComponentByUid(div.uid);
1404
1405 expect(info.type).toBe('host');
1406 expect(info.name).toBe('div');
1407 expect(info.props.className).toBe('app');
1408 expect(info.props.id).toBe('root');
1409 });
1410
1411 it('uses uids consistent with getComponentTree', () => {
1412 function Header() {
1413 return <h1>title</h1>;
1414 }
1415 function Footer() {
1416 return <footer>foot</footer>;
1417 }
1418 function App() {
1419 return (
1420 <div>
1421 <Header />
1422 <Footer />
1423 </div>
1424 );
1425 }
1426
1427 act(() => {
1428 ReactDOMClient.createRoot(container).render(<App />);
1429 });
1430
1431 const nodes = getComponentTree();
1432 nodes.forEach(node => {
1433 const info = getComponentByUid(node.uid);
1434 expect(info.uid).toBe(node.uid);
1435 });
1436 });
1437
1438 it('normalizes nested objects and arrays in props', () => {
1439 function Config() {
1440 return <div>config</div>;
1441 }
1442
1443 act(() => {
1444 ReactDOMClient.createRoot(container).render(
1445 <Config style={{color: 'red', fontSize: 14}} items={[1, 2, 3]} />,
1446 );
1447 });
1448
1449 const config = getComponentTree().find(n => n.name === 'Config');
1450 const info = getComponentByUid(config.uid);
1451 expect(info.props.style).toEqual({color: 'red', fontSize: 14});
1452 expect(info.props.items).toEqual([1, 2, 3]);
1453 });
1454
1455 it('normalizes symbol and undefined props', () => {
1456 function Widget() {
1457 return <div>w</div>;
1458 }
1459
1460 act(() => {
1461 ReactDOMClient.createRoot(container).render(
1462 <Widget sym={Symbol('test')} undef={undefined} />,
1463 );
1464 });
1465
1466 const widget = getComponentTree().find(n => n.name === 'Widget');
1467 const info = getComponentByUid(widget.uid);
1468 expect(info.props.sym).toBe('[symbol]');
1469 expect(info.props.undef).toBe(null);
1470 });
1471
1472 it('returns info for Memo component with correct type', () => {
1473 function Inner() {
1474 return <span>inner</span>;
1475 }
1476 const Memoized = React.memo(Inner);
1477
1478 act(() => {
1479 ReactDOMClient.createRoot(container).render(<Memoized value={42} />);
1480 });
1481
1482 const memo = getComponentTree().find(n => n.type === 'memo');
1483 expect(memo).toBeDefined();
1484 const info = getComponentByUid(memo.uid);
1485 expect(info.type).toBe('memo');
1486 });
1487
1488 it('returns info for ForwardRef component with correct type', () => {
1489 const FancyInput = React.forwardRef(function FancyInput(props, ref) {
1490 return <input ref={ref} />;
1491 });
1492
1493 act(() => {
1494 ReactDOMClient.createRoot(container).render(<FancyInput />);
1495 });
1496
1497 const fwd = getComponentTree().find(n => n.type === 'forwardRef');
1498 expect(fwd).toBeDefined();
1499 const info = getComponentByUid(fwd.uid);
1500 expect(info.type).toBe('forwardRef');
1501 });
1502
1503 it('returns no props when component has only children', () => {
1504 function Wrapper() {
1505 return <div>child</div>;
1506 }
1507
1508 act(() => {
1509 ReactDOMClient.createRoot(container).render(<Wrapper />);
1510 });
1511
1512 const wrapper = getComponentTree().find(n => n.name === 'Wrapper');
1513 const info = getComponentByUid(wrapper.uid);
1514 // No props key at all (children are excluded)
1515 expect(info.props).toBeUndefined();
1516 });
1517
1518 it('handles circular references in props without stack overflow', () => {
1519 function Widget() {
1520 return <div>widget</div>;
1521 }
1522
1523 const circular = {a: 1};
1524 circular.self = circular;
1525
1526 act(() => {
1527 ReactDOMClient.createRoot(container).render(<Widget data={circular} />);
1528 });
1529
1530 const widget = getComponentTree().find(n => n.name === 'Widget');
1531 // Should not throw or stack overflow
1532 const info = getComponentByUid(widget.uid);
1533 expect(info.props.data.a).toBe(1);
1534 expect(info.props.data.self).toBe('[circular]');
1535 });
1536
1537 it('handles deeply nested objects in props without stack overflow', () => {
1538 function Widget() {
1539 return <div>widget</div>;
1540 }
1541
1542 // Create a very deeply nested object
1543 let deep = {value: 'leaf'};
1544 for (let i = 0; i < 200; i++) {
1545 deep = {nested: deep};
1546 }
1547
1548 act(() => {
1549 ReactDOMClient.createRoot(container).render(<Widget data={deep} />);
1550 });
1551
1552 const widget = getComponentTree().find(n => n.name === 'Widget');
1553 // Should not throw or stack overflow
1554 const info = getComponentByUid(widget.uid);
1555 expect(info.props.data).toBeDefined();
1556 });
1557
1558 it('returns the full hooks tree for a function component', () => {
1559 function useCounter() {
1560 const [c] = React.useState(0);
1561 return c;
1562 }
1563 function Widget() {
1564 const [count] = React.useState(7);
1565 React.useEffect(() => {}, []);
1566 const [obj] = React.useState({color: 'red'});
1567 useCounter();
1568 const ref = React.useRef(1);
1569 const memo = React.useMemo(() => 5, []);
1570 return (
1571 <div>
1572 {count}
1573 {obj.color}
1574 {ref.current}
1575 {memo}
1576 </div>
1577 );
1578 }
1579
1580 act(() => {
1581 ReactDOMClient.createRoot(container).render(<Widget />);
1582 });
1583
1584 const widget = getComponentTree().find(n => n.name === 'Widget');
1585 const info = getComponentByUid(widget.uid, true);
1586
1587 // Full structural assertion: every hook node, in order, with its id
1588 // (sequential per primitive hook; custom hooks are null), name, normalized
1589 // value (the Effect's create fn becomes '[fn]'), and subHooks.
1590 expect(info.hooks).toEqual([
1591 {id: 0, name: 'State', value: 7, subHooks: []},
1592 {id: 1, name: 'Effect', value: '[fn]', subHooks: []},
1593 {id: 2, name: 'State', value: {color: 'red'}, subHooks: []},
1594 {
1595 id: null,
1596 name: 'Counter',
1597 value: null,
1598 subHooks: [{id: 3, name: 'State', value: 0, subHooks: []}],
1599 },
1600 {id: 4, name: 'Ref', value: 1, subHooks: []},
1601 {id: 5, name: 'Memo', value: 5, subHooks: []},
1602 ]);
1603 });
1604
1605 it('does not inspect hooks by default', () => {
1606 function Widget() {
1607 React.useState(7);
1608 return <div>widget</div>;
1609 }
1610
1611 act(() => {
1612 ReactDOMClient.createRoot(container).render(<Widget />);
1613 });
1614
1615 const widget = getComponentTree().find(n => n.name === 'Widget');
1616 const info = getComponentByUid(widget.uid);
1617
1618 expect(info.hooks).toBeUndefined();
1619 });
1620
1621 it('returns an error when requested hook inspection fails', () => {
1622 jest.resetModules();
1623 jest.doMock('react-debug-tools', () => ({
1624 inspectHooksOfFiberWithoutDefaultDispatcher() {
1625 throw new Error('Cannot inspect hooks');
1626 },
1627 }));
1628 delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1629
1630 const facadeAPI = require('../../index');
1631 const mockedFacade = facadeAPI.installFacade();
1632 const mockedTools = facadeAPI.createTools(mockedFacade);
1633 const MockedReact = require('react');
1634 const MockedReactDOMClient = require('react-dom/client');
1635
1636 function Widget() {
1637 MockedReact.useState(7);
1638 return MockedReact.createElement('div', null, 'widget');
1639 }
1640
1641 MockedReact.act(() => {
1642 MockedReactDOMClient.createRoot(container).render(
1643 MockedReact.createElement(Widget),
1644 );
1645 });
1646
1647 const widget = mockedTools
1648 .getComponentTree()
1649 .find(node => node.name === 'Widget');
1650 const info = mockedTools.getComponentByUid(widget.uid, true);
1651
1652 expect(info.error).toBeInstanceOf(Error);
1653 expect(info.error.message).toBe('Failed to inspect hooks.');
1654 expect(info.error.cause).toEqual(new Error('Cannot inspect hooks'));
1655 });
1656
1657 it('captures the useContext hook with its provided value', () => {
1658 const ThemeContext = React.createContext('light');
1659 function Themed() {
1660 const theme = React.useContext(ThemeContext);
1661 const [count] = React.useState(0);
1662 return (
1663 <div>
1664 {theme}
1665 {count}
1666 </div>
1667 );
1668 }
1669 function App() {
1670 return (
1671 <ThemeContext value="dark">
1672 <Themed />
1673 </ThemeContext>
1674 );
1675 }
1676
1677 act(() => {
1678 ReactDOMClient.createRoot(container).render(<App />);
1679 });
1680
1681 const themed = getComponentTree().find(n => n.name === 'Themed');
1682 const info = getComponentByUid(themed.uid, true);
1683 // useContext is captured as a "Context" hook holding the provider's value.
1684 // It does not consume a primitive hook slot, so its id is null; the
1685 // following useState is the first primitive hook (id 0).
1686 expect(info.hooks).toEqual([
1687 {id: null, name: 'Context', value: 'dark', subHooks: []},
1688 {id: 0, name: 'State', value: 0, subHooks: []},
1689 ]);
1690 });
1691
1692 it('returns an empty hooks array for a function component with no hooks', () => {
1693 function Plain() {
1694 return <div>plain</div>;
1695 }
1696
1697 act(() => {
1698 ReactDOMClient.createRoot(container).render(<Plain />);
1699 });
1700
1701 const plain = getComponentTree().find(n => n.name === 'Plain');
1702 const info = getComponentByUid(plain.uid, true);
1703 expect(info.hooks).toEqual([]);
1704 });
1705
1706 it('does not include hooks for class components', () => {
1707 class MyClass extends React.Component {
1708 render() {
1709 return <div>class</div>;
1710 }
1711 }
1712
1713 act(() => {
1714 ReactDOMClient.createRoot(container).render(<MyClass />);
1715 });
1716
1717 const myClass = getComponentTree().find(n => n.name === 'MyClass');
1718 const info = getComponentByUid(myClass.uid, true);
1719 expect(info.hooks).toBeUndefined();
1720 });
1721
1722 it('does not include hooks for host components', () => {
1723 function App() {
1724 return <div>hello</div>;
1725 }
1726
1727 act(() => {
1728 ReactDOMClient.createRoot(container).render(<App />);
1729 });
1730
1731 const div = getComponentTree().find(n => n.name === 'div');
1732 const info = getComponentByUid(div.uid, true);
1733 expect(info.hooks).toBeUndefined();
1734 });
1735 });
1736
1737 describe('getComponentByHostInstance', () => {
1738 let getComponentTree;
1739 let getComponentByUid;
1740 let getComponentByHostInstance;
1741
1742 beforeEach(() => {
1743 const tools = createTools(facade);
1744 getComponentTree = tools.getComponentTree;
1745 getComponentByUid = tools.getComponentByUid;
1746 getComponentByHostInstance = tools.getComponentByHostInstance;
1747 });
1748
1749 it('returns the host component for a DOM host element', () => {
1750 function Child({label}) {
1751 return <span className="leaf">{label}</span>;
1752 }
1753 function App() {
1754 return (
1755 <div>
1756 <Child label="leaf" />
1757 </div>
1758 );
1759 }
1760
1761 act(() => {
1762 ReactDOMClient.createRoot(container).render(<App />);
1763 });
1764
1765 const span = container.querySelector('span.leaf');
1766 const host = getComponentTree().find(n => n.name === 'span');
1767 const result = getComponentByHostInstance(span);
1768
1769 expect(result).toEqual(getComponentByUid(host.uid));
1770 expect(result).toMatchObject({
1771 uid: host.uid,
1772 type: 'host',
1773 name: 'span',
1774 props: {className: 'leaf'},
1775 });
1776 });
1777
1778 it('returns the host component rather than the tree owner', () => {
1779 function Wrapper({children}) {
1780 return <section className="wrap">{children}</section>;
1781 }
1782 function App() {
1783 return (
1784 <Wrapper>
1785 <button className="action">Run</button>
1786 </Wrapper>
1787 );
1788 }
1789
1790 act(() => {
1791 ReactDOMClient.createRoot(container).render(<App />);
1792 });
1793
1794 const button = container.querySelector('button.action');
1795 const tree = getComponentTree();
1796 const host = tree.find(n => n.name === 'button');
1797 const wrapper = tree.find(n => n.name === 'Wrapper');
1798 const app = tree.find(n => n.name === 'App');
1799 const result = getComponentByHostInstance(button);
1800
1801 expect(result.uid).toBe(host.uid);
1802 expect(result.uid).not.toBe(wrapper.uid);
1803 expect(result.uid).not.toBe(app.uid);
1804 expect(result).toMatchObject({
1805 type: 'host',
1806 name: 'button',
1807 props: {className: 'action'},
1808 });
1809 });
1810
1811 it('keeps uids stable across re-renders via alternate fibers', () => {
1812 function Counter({count}) {
1813 return <div className="counter">{'Count: ' + count}</div>;
1814 }
1815
1816 const root = ReactDOMClient.createRoot(container);
1817 act(() => {
1818 root.render(<Counter count={0} />);
1819 });
1820
1821 const div = container.querySelector('div.counter');
1822 const first = getComponentByHostInstance(div);
1823
1824 act(() => {
1825 root.render(<Counter count={1} />);
1826 });
1827
1828 const second = getComponentByHostInstance(div);
1829 expect(second.uid).toBe(first.uid);
1830 expect(second.name).toBe('div');
1831 expect(second.type).toBe('host');
1832 expect(second.props.className).toBe('counter');
1833 });
1834
1835 it('does not walk platform parent pointers for unmanaged nested nodes', () => {
1836 function App() {
1837 return <div className="host" />;
1838 }
1839
1840 act(() => {
1841 ReactDOMClient.createRoot(container).render(<App />);
1842 });
1843
1844 const host = container.querySelector('div.host');
1845 const unmanagedChild = document.createElement('i');
1846 host.appendChild(unmanagedChild);
1847
1848 expect(getComponentByHostInstance(unmanagedChild)).toEqual({
1849 error: 'Host instance is not managed by React',
1850 });
1851 });
1852
1853 it('returns an error when no roots are mounted', () => {
1854 expect(getComponentByHostInstance({})).toEqual({
1855 error: 'No mounted React roots found',
1856 });
1857 });
1858
1859 it('returns an error for null or undefined references', () => {
1860 expect(getComponentByHostInstance(null)).toEqual({
1861 error: 'Host instance is required',
1862 });
1863 expect(getComponentByHostInstance(undefined)).toEqual({
1864 error: 'Host instance is required',
1865 });
1866 });
1867 });
1868
1869 describe('profiler', () => {
1870 let startProfiling;
1871 let stopProfiling;
1872 let getTraceOverview;
1873 let getCommitReport;
1874 let getComponentTree;
1875 let getComponentByUid;
1876
1877 beforeEach(() => {
1878 const tools = createTools(facade);
1879 startProfiling = tools.startProfiling;
1880 stopProfiling = tools.stopProfiling;
1881 getTraceOverview = tools.getTraceOverview;
1882 getCommitReport = tools.getCommitReport;
1883 getComponentTree = tools.getComponentTree;
1884 getComponentByUid = tools.getComponentByUid;
1885 });
1886
1887 it('startProfiling returns the started status and trace name', () => {
1888 expect(startProfiling('my-trace')).toEqual({
1889 status: 'started',
1890 traceName: 'my-trace',
1891 });
1892 stopProfiling();
1893 });
1894
1895 it('startProfiling auto-generates a trace name when none is provided', () => {
1896 const result = startProfiling();
1897 expect(result.status).toBe('started');
1898 expect(result.traceName).toMatch(/^trace-\d+$/);
1899 stopProfiling();
1900 });
1901
1902 it('stopProfiling reports the trace name and commit count', () => {
1903 startProfiling('test-trace');
1904 expect(stopProfiling()).toEqual({
1905 status: 'stopped',
1906 traceName: 'test-trace',
1907 commits: 0,
1908 });
1909 });
1910
1911 it('cannot start profiling twice', () => {
1912 startProfiling('first');
1913 expect(startProfiling('second')).toEqual({
1914 error: 'Already profiling trace "first"',
1915 });
1916 stopProfiling();
1917 });
1918
1919 it('cannot stop when not profiling', () => {
1920 expect(stopProfiling()).toEqual({error: 'Not currently profiling'});
1921 });
1922
1923 it('records one commit per render and reports the count on stop', () => {
1924 function Counter({count}) {
1925 return <div>{'Count: ' + count}</div>;
1926 }
1927
1928 const root = ReactDOMClient.createRoot(container);
1929 act(() => {
1930 root.render(<Counter count={0} />);
1931 });
1932
1933 startProfiling('render-trace');
1934 act(() => {
1935 root.render(<Counter count={1} />);
1936 });
1937 act(() => {
1938 root.render(<Counter count={2} />);
1939 });
1940
1941 expect(stopProfiling()).toEqual({
1942 status: 'stopped',
1943 traceName: 'render-trace',
1944 commits: 2,
1945 });
1946 });
1947
1948 it('getTraceOverview returns one row per commit', () => {
1949 function Child() {
1950 return <span>child</span>;
1951 }
1952 function Counter({count}) {
1953 return (
1954 <div>
1955 <Child />
1956 {count}
1957 </div>
1958 );
1959 }
1960
1961 const root = ReactDOMClient.createRoot(container);
1962 act(() => {
1963 root.render(<Counter count={0} />);
1964 });
1965
1966 startProfiling('overview-trace');
1967 act(() => {
1968 root.render(<Counter count={1} />);
1969 });
1970 act(() => {
1971 root.render(<Counter count={2} />);
1972 });
1973 stopProfiling();
1974
1975 const overview = getTraceOverview('overview-trace');
1976 expect(overview).toHaveLength(2);
1977 let previousCommittedAt = 0;
1978 overview.forEach((row, i) => {
1979 expect(row.commit).toBe(i);
1980 // committedAt is relative to trace start: non-negative and monotonic.
1981 expect(row.committedAt).toBeGreaterThanOrEqual(previousCommittedAt);
1982 previousCommittedAt = row.committedAt;
1983 // componentsChanged matches the commit report's component count.
1984 expect(row.componentsChanged).toBe(
1985 getCommitReport('overview-trace', i).components.length,
1986 );
1987 expect(isDuration(row.renderDuration)).toBe(true);
1988 expect(isDuration(row.layoutDuration)).toBe(true);
1989 expect(isDuration(row.passiveDuration)).toBe(true);
1990 });
1991 });
1992
1993 it('getTraceOverview returns an error for an unknown trace', () => {
1994 expect(getTraceOverview('nope')).toEqual({error: 'Unknown trace "nope"'});
1995 });
1996
1997 it('getTraceOverview returns an empty array for a trace with no commits', () => {
1998 startProfiling('empty-trace');
1999 stopProfiling();
2000 expect(getTraceOverview('empty-trace')).toEqual([]);
2001 });
2002
2003 it('getCommitReport returns commit metadata and the full component set', () => {
2004 function Child() {
2005 return <span>child</span>;
2006 }
2007 function Counter({count}) {
2008 return (
2009 <div>
2010 <Child />
2011 {count}
2012 </div>
2013 );
2014 }
2015
2016 const root = ReactDOMClient.createRoot(container);
2017 act(() => {
2018 root.render(<Counter count={0} />);
2019 });
2020
2021 startProfiling('detail-trace');
2022 act(() => {
2023 root.render(<Counter count={1} />);
2024 });
2025 stopProfiling();
2026
2027 const report = getCommitReport('detail-trace', 0);
2028 expect(report.priority).toBe('Normal');
2029 expect(report.committedAt).toBeGreaterThanOrEqual(0);
2030 expect(isDuration(report.renderDuration)).toBe(true);
2031 expect(isDuration(report.layoutDuration)).toBe(true);
2032 expect(isDuration(report.passiveDuration)).toBe(true);
2033
2034 // The exact set of components that rendered. Order is duration-dependent
2035 // (sorted descending), so compare sorted by name.
2036 const byName = report.components
2037 .map(c => ({name: c.name, type: c.type}))
2038 .sort((a, b) => a.name.localeCompare(b.name));
2039 expect(byName).toEqual([
2040 {name: 'Child', type: 'function'},
2041 {name: 'Counter', type: 'function'},
2042 {name: 'createRoot()', type: 'root'},
2043 {name: 'div', type: 'host'},
2044 {name: 'span', type: 'host'},
2045 ]);
2046 report.components.forEach(c => {
2047 expect(c.uid).toMatch(/^r\d+$/);
2048 expect(isDuration(c.actualDuration)).toBe(true);
2049 expect(isDuration(c.selfDuration)).toBe(true);
2050 });
2051 });
2052
2053 it('getCommitReport sorts components by actualDuration descending', () => {
2054 function Child() {
2055 return <span>child</span>;
2056 }
2057 function Counter({count}) {
2058 return (
2059 <div>
2060 <Child />
2061 {count}
2062 </div>
2063 );
2064 }
2065
2066 const root = ReactDOMClient.createRoot(container);
2067 act(() => {
2068 root.render(<Counter count={0} />);
2069 });
2070 startProfiling('sort-trace');
2071 act(() => {
2072 root.render(<Counter count={1} />);
2073 });
2074 stopProfiling();
2075
2076 const durations = getCommitReport('sort-trace', 0).components.map(
2077 c => c.actualDuration || 0,
2078 );
2079 for (let i = 1; i < durations.length; i++) {
2080 expect(durations[i]).toBeLessThanOrEqual(durations[i - 1]);
2081 }
2082 });
2083
2084 it('getCommitReport committedAt matches getTraceOverview', () => {
2085 function Counter({count}) {
2086 return <div>{'Count: ' + count}</div>;
2087 }
2088
2089 const root = ReactDOMClient.createRoot(container);
2090 act(() => {
2091 root.render(<Counter count={0} />);
2092 });
2093 startProfiling('match-trace');
2094 act(() => {
2095 root.render(<Counter count={1} />);
2096 });
2097 stopProfiling();
2098
2099 const overview = getTraceOverview('match-trace');
2100 const report = getCommitReport('match-trace', 0);
2101 expect(report.committedAt).toBe(overview[0].committedAt);
2102 });
2103
2104 it('getCommitReport returns an error for an unknown trace', () => {
2105 expect(getCommitReport('nope', 0)).toEqual({
2106 error: 'Unknown trace "nope"',
2107 });
2108 });
2109
2110 it('getCommitReport returns an error for an out-of-range commit index', () => {
2111 startProfiling('range-trace');
2112 stopProfiling();
2113 expect(getCommitReport('range-trace', 5)).toEqual({
2114 error: 'Commit index out of range',
2115 });
2116 expect(getCommitReport('range-trace', -1)).toEqual({
2117 error: 'Commit index out of range',
2118 });
2119 });
2120
2121 it('does not record internal nodes like Fragment, Mode, or text', () => {
2122 function Child() {
2123 return <span>child</span>;
2124 }
2125 function App() {
2126 return (
2127 <React.StrictMode>
2128 <React.Fragment>
2129 <Child />
2130 </React.Fragment>
2131 </React.StrictMode>
2132 );
2133 }
2134
2135 const root = ReactDOMClient.createRoot(container);
2136 act(() => {
2137 root.render(<App />);
2138 });
2139 startProfiling('internal-trace');
2140 act(() => {
2141 root.render(<App />);
2142 });
2143 stopProfiling();
2144
2145 const names = getCommitReport('internal-trace', 0).components.map(
2146 c => c.name,
2147 );
2148 expect(names).not.toContain('Fragment');
2149 expect(names).not.toContain('StrictMode');
2150 // Only named components are recorded; no Unknown/internal entries.
2151 names.forEach(name => {
2152 expect(typeof name).toBe('string');
2153 expect(name).not.toBe('Unknown');
2154 });
2155 });
2156
2157 it('uses uids consistent with the tree tools', () => {
2158 function Widget() {
2159 return <div>widget</div>;
2160 }
2161
2162 const root = ReactDOMClient.createRoot(container);
2163 act(() => {
2164 root.render(<Widget />);
2165 });
2166 const widget = getComponentTree().find(n => n.name === 'Widget');
2167
2168 startProfiling('uid-trace');
2169 act(() => {
2170 root.render(<Widget />);
2171 });
2172 stopProfiling();
2173
2174 const report = getCommitReport('uid-trace', 0);
2175 const widgetEntry = report.components.find(c => c.name === 'Widget');
2176 expect(widgetEntry).toBeDefined();
2177 expect(widgetEntry.uid).toBe(widget.uid);
2178 // ...and the same uid resolves back through getComponentByUid.
2179 expect(getComponentByUid(widget.uid).name).toBe('Widget');
2180 });
2181
2182 it('records commits across multiple independent traces', () => {
2183 function Counter({count}) {
2184 return <div>{'Count: ' + count}</div>;
2185 }
2186
2187 const root = ReactDOMClient.createRoot(container);
2188 act(() => {
2189 root.render(<Counter count={0} />);
2190 });
2191
2192 startProfiling('trace-a');
2193 act(() => {
2194 root.render(<Counter count={1} />);
2195 });
2196 stopProfiling();
2197
2198 startProfiling('trace-b');
2199 act(() => {
2200 root.render(<Counter count={2} />);
2201 });
2202 act(() => {
2203 root.render(<Counter count={3} />);
2204 });
2205 stopProfiling();
2206
2207 expect(getTraceOverview('trace-a')).toHaveLength(1);
2208 expect(getTraceOverview('trace-b')).toHaveLength(2);
2209 });
2210
2211 it('the hook onPostCommitFiberRoot is a no-op when not profiling', () => {
2212 const hook = facade.hook;
2213 expect(typeof hook.onPostCommitFiberRoot).toBe('function');
2214 expect(() => {
2215 hook.onPostCommitFiberRoot(0, {passiveEffectDuration: 0});
2216 }).not.toThrow();
2217 });
2218 });
2219
2220 describe('multiple roots and renderers', () => {
2221 it('inject() registers a new renderer and initializes its internals', () => {
2222 // react-dom registered itself as a renderer when it was required. Here we
2223 // register a second (simulated) renderer to cover the registration
2224 // contract; the cross-root tests below use a single react-dom renderer.
2225 const before = facade.hook.renderers.size;
2226 const id = facade.hook.inject({
2227 reconcilerVersion: '18.2.0',
2228 version: '18.2.0',
2229 });
2230 expect(typeof id).toBe('number');
2231 expect(facade.hook.renderers.size).toBe(before + 1);
2232 expect(facade.rendererInternals.has(id)).toBe(true);
2233 });
2234
2235 it('getComponentTree aggregates components from multiple roots', () => {
2236 function AppA() {
2237 return <div>A</div>;
2238 }
2239 function AppB() {
2240 return <div>B</div>;
2241 }
2242
2243 const containerB = document.createElement('div');
2244
2245 act(() => {
2246 ReactDOMClient.createRoot(container).render(<AppA />);
2247 ReactDOMClient.createRoot(containerB).render(<AppB />);
2248 });
2249
2250 const tree = createTools(facade).getComponentTree();
2251 // Two roots → exactly two HostRoot nodes.
2252 expect(tree.filter(n => n.type === 'root')).toHaveLength(2);
2253
2254 const appA = tree.find(n => n.name === 'AppA');
2255 const appB = tree.find(n => n.name === 'AppB');
2256 // Uids come from a per-call counter that spans every root, in root
2257 // order: root A gets r0–r2, root B gets r3–r5, so uids are
2258 // globally unique across roots.
2259 expect(appA).toEqual({
2260 uid: 'r0',
2261 type: 'function',
2262 name: 'AppA',
2263 key: null,
2264 firstChild: 'r2',
2265 nextSibling: null,
2266 });
2267 expect(appB).toEqual({
2268 uid: 'r3',
2269 type: 'function',
2270 name: 'AppB',
2271 key: null,
2272 firstChild: 'r5',
2273 nextSibling: null,
2274 });
2275 });
2276
2277 it('findComponents finds matches across multiple roots', () => {
2278 function Shared() {
2279 return <span>shared</span>;
2280 }
2281 function RootA() {
2282 return <Shared />;
2283 }
2284 function RootB() {
2285 return <Shared />;
2286 }
2287
2288 const containerB = document.createElement('div');
2289 act(() => {
2290 ReactDOMClient.createRoot(container).render(<RootA />);
2291 ReactDOMClient.createRoot(containerB).render(<RootB />);
2292 });
2293
2294 const result = createTools(facade).findComponents('Shared');
2295 expect(result.totalCount).toBe(2);
2296 expect(result.results.map(r => r.name)).toEqual(['Shared', 'Shared']);
2297 // Globally unique uids, assigned in result order across both roots.
2298 expect(result.results.map(r => r.uid)).toEqual(['r0', 'r2']);
2299 });
2300
2301 it('resolves uids from any root via getComponentByUid', () => {
2302 function Widget() {
2303 return <div>w</div>;
2304 }
2305 function RootA() {
2306 return <Widget />;
2307 }
2308 function RootB() {
2309 return <Widget />;
2310 }
2311
2312 const containerB = document.createElement('div');
2313 act(() => {
2314 ReactDOMClient.createRoot(container).render(<RootA />);
2315 ReactDOMClient.createRoot(containerB).render(<RootB />);
2316 });
2317
2318 const tools = createTools(facade);
2319 const widgets = tools.findComponents('Widget').results;
2320 expect(widgets).toHaveLength(2);
2321 widgets.forEach(w => {
2322 expect(tools.getComponentByUid(w.uid).name).toBe('Widget');
2323 });
2324 });
2325
2326 it('profiling records commits from all roots', () => {
2327 function CounterA({count}) {
2328 return <div>{'A:' + count}</div>;
2329 }
2330 function CounterB({count}) {
2331 return <div>{'B:' + count}</div>;
2332 }
2333
2334 const containerB = document.createElement('div');
2335
2336 const rootA = ReactDOMClient.createRoot(container);
2337 const rootB = ReactDOMClient.createRoot(containerB);
2338 act(() => {
2339 rootA.render(<CounterA count={0} />);
2340 rootB.render(<CounterB count={0} />);
2341 });
2342
2343 const tools = createTools(facade);
2344 tools.startProfiling('multi-root-trace');
2345 act(() => {
2346 rootA.render(<CounterA count={1} />);
2347 });
2348 act(() => {
2349 rootB.render(<CounterB count={1} />);
2350 });
2351
2352 expect(tools.stopProfiling()).toEqual({
2353 status: 'stopped',
2354 traceName: 'multi-root-trace',
2355 commits: 2,
2356 });
2357
2358 const overview = tools.getTraceOverview('multi-root-trace');
2359 expect(overview).toHaveLength(2);
2360 // Separate act() blocks force exactly one commit each, in order, so
2361 // commit 0 is rootA's re-render and commit 1 is rootB's.
2362 const names0 = tools
2363 .getCommitReport('multi-root-trace', 0)
2364 .components.map(c => c.name);
2365 const names1 = tools
2366 .getCommitReport('multi-root-trace', 1)
2367 .components.map(c => c.name);
2368 expect(names0).toContain('CounterA');
2369 expect(names0).not.toContain('CounterB');
2370 expect(names1).toContain('CounterB');
2371 expect(names1).not.toContain('CounterA');
2372 });
2373 });
2374
2375 describe('with the React DevTools extension hook already installed', () => {
2376 // Simulate the extension: a real React DevTools hook is installed, and React
2377 // registers with it, before the facade attaches. Re-set up the module graph
2378 // so react-dom injects into this hook rather than the facade's own.
2379 let localContainer;
2380
2381 beforeEach(() => {
2382 jest.resetModules();
2383 global.IS_REACT_ACT_ENVIRONMENT = true;
2384 delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2385 require('react-devtools-shared/src/hook').installHook(window, []);
2386
2387 const facadeAPI = require('../../index');
2388 installFacade = facadeAPI.installFacade;
2389 createTools = facadeAPI.createTools;
2390 React = require('react');
2391 ReactDOMClient = require('react-dom/client');
2392 act = React.act;
2393
2394 localContainer = document.createElement('div');
2395 document.body.appendChild(localContainer);
2396 });
2397
2398 afterEach(() => {
2399 document.body.removeChild(localContainer);
2400 localContainer = null;
2401 });
2402
2403 it('attaches to the extension hook and reads its component tree', () => {
2404 const extensionHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
2405
2406 function Child() {
2407 return <span>c</span>;
2408 }
2409 function App() {
2410 return (
2411 <div>
2412 <Child />
2413 </div>
2414 );
2415 }
2416
2417 act(() => {
2418 ReactDOMClient.createRoot(localContainer).render(<App />);
2419 });
2420
2421 const localFacade = installFacade();
2422 // Attached to the extension's hook rather than replacing it.
2423 expect(localFacade.hook).toBe(extensionHook);
2424
2425 const tree = createTools(localFacade).getComponentTree();
2426 expect(tree.find(n => n.name === 'App')).toBeDefined();
2427 expect(tree.find(n => n.name === 'Child')).toBeDefined();
2428 });
2429 });
2430 });