8
'use strict';
9
10
let installFacade;
11
+let createTools;
12
let facade;
13
let React;
14
let ReactDOMClient;
28
29
// Install the facade BEFORE React so the hook captures the first commit.
30
// Import through the package entry point to exercise the public surface.
30
- installFacade = require('../../index').installFacade;
31
+ const facadeAPI = require('../../index');
32
+ installFacade = facadeAPI.installFacade;
33
+ createTools = facadeAPI.createTools;
34
facade = installFacade();
35
36
React = require('react');
125
126
expect(facade.hook.getFiberRoots(rendererID).size).toBe(0);
127
});
128
+
129
+ describe('getComponentTree', () => {
130
+ let getComponentTree;
131
+
132
+ beforeEach(() => {
133
+ getComponentTree = createTools(facade).getComponentTree;
134
+ });
135
+
136
+ it('returns error when nothing is rendered', () => {
137
+ const result = getComponentTree();
138
+ expect(result.error).toMatch(/No mounted React roots found/);
139
+ });
140
+
141
+ it('returns an array of component nodes', () => {
142
+ function App() {
143
+ return <div>hello</div>;
144
+ }
145
+
146
+ act(() => {
147
+ ReactDOMClient.createRoot(container).render(<App />);
148
+ });
149
+
150
+ const result = getComponentTree();
151
+ expect(Array.isArray(result)).toBe(true);
152
+ const app = result.find(n => n.name === 'App');
153
+ const div = result.find(n => n.name === 'div');
154
+ // App is the root's only child; its child is the host div.
155
+ expect(app).toEqual({
156
+ uid: 'r0',
157
+ type: 'function',
158
+ name: 'App',
159
+ key: null,
160
+ firstChild: div.uid,
161
+ nextSibling: null,
162
+ });
163
+ // A single string child ('hello') is stored as a prop, not a child fiber,
164
+ // so the div is a leaf in the tree.
165
+ expect(div).toEqual({
166
+ uid: 'r2',
167
+ type: 'host',
168
+ name: 'div',
169
+ key: null,
170
+ firstChild: null,
171
+ nextSibling: null,
172
+ });
173
+ });
174
+
175
+ it('encodes firstChild and nextSibling relationships', () => {
176
+ function Header() {
177
+ return <h1>title</h1>;
178
+ }
179
+ function Footer() {
180
+ return <footer>foot</footer>;
181
+ }
182
+ function App() {
183
+ return (
184
+ <div>
185
+ <Header />
186
+ <Footer />
187
+ </div>
188
+ );
189
+ }
190
+
191
+ act(() => {
192
+ ReactDOMClient.createRoot(container).render(<App />);
193
+ });
194
+
195
+ const nodes = getComponentTree();
196
+ const app = nodes.find(n => n.name === 'App');
197
+ const div = nodes.find(n => n.name === 'div');
198
+ const header = nodes.find(n => n.name === 'Header');
199
+ const footer = nodes.find(n => n.name === 'Footer');
200
+
201
+ // App's firstChild is div
202
+ expect(app.firstChild).toBe(div.uid);
203
+ // div's firstChild is Header
204
+ expect(div.firstChild).toBe(header.uid);
205
+ // Header's nextSibling is Footer
206
+ expect(header.nextSibling).toBe(footer.uid);
207
+ // Footer has no nextSibling
208
+ expect(footer.nextSibling).toBe(null);
209
+ });
210
+
211
+ it('shows keys in the output', () => {
212
+ function Item() {
213
+ return <li>item</li>;
214
+ }
215
+ function List() {
216
+ return (
217
+ <ul>
218
+ <Item key="a" />
219
+ <Item key="b" />
220
+ </ul>
221
+ );
222
+ }
223
+
224
+ act(() => {
225
+ ReactDOMClient.createRoot(container).render(<List />);
226
+ });
227
+
228
+ const items = getComponentTree().filter(n => n.name === 'Item');
229
+ expect(items.map(i => i.key)).toEqual(['a', 'b']);
230
+ });
231
+
232
+ it('limits depth with the depth parameter', () => {
233
+ function Child() {
234
+ return <span>leaf</span>;
235
+ }
236
+ function Parent() {
237
+ return <Child />;
238
+ }
239
+ function App() {
240
+ return <Parent />;
241
+ }
242
+
243
+ act(() => {
244
+ ReactDOMClient.createRoot(container).render(<App />);
245
+ });
246
+
247
+ const names = snapshot => snapshot.map(n => n.name);
248
+
249
+ // depth=0: only the root node (HostRoot)
250
+ const shallow = getComponentTree(0);
251
+ expect(shallow).toHaveLength(1);
252
+ expect(shallow[0].type).toBe('root');
253
+
254
+ // depth=1: root + App
255
+ const d1 = getComponentTree(1);
256
+ expect(names(d1)).toContain('App');
257
+ expect(names(d1)).not.toContain('Parent');
258
+
259
+ // depth=2: root + App + Parent
260
+ const d2 = getComponentTree(2);
261
+ expect(names(d2)).toContain('App');
262
+ expect(names(d2)).toContain('Parent');
263
+ expect(names(d2)).not.toContain('Child');
264
+
265
+ const deep = getComponentTree(20);
266
+ expect(names(deep)).toEqual(
267
+ expect.arrayContaining(['App', 'Parent', 'Child']),
268
+ );
269
+ });
270
+
271
+ it('starts from a specific node when rootUid is provided', () => {
272
+ function Nav() {
273
+ return <nav>nav</nav>;
274
+ }
275
+ function Header() {
276
+ return <Nav />;
277
+ }
278
+ function Footer() {
279
+ return <footer>foot</footer>;
280
+ }
281
+ function App() {
282
+ return (
283
+ <div>
284
+ <Header />
285
+ <Footer />
286
+ </div>
287
+ );
288
+ }
289
+
290
+ act(() => {
291
+ ReactDOMClient.createRoot(container).render(<App />);
292
+ });
293
+
294
+ // First, get the full tree to find Header's uid
295
+ const header = getComponentTree().find(n => n.name === 'Header');
296
+ expect(header).toBeDefined();
297
+
298
+ // Snapshot from Header
299
+ const sub = getComponentTree(20, header.uid);
300
+ const names = sub.map(n => n.name);
301
+ expect(names).toContain('Header');
302
+ expect(names).toContain('Nav');
303
+ // Should NOT contain App or Footer
304
+ expect(names).not.toContain('App');
305
+ expect(names).not.toContain('Footer');
306
+ });
307
+
308
+ it('returns error for non-existent rootUid', () => {
309
+ function App() {
310
+ return <div>hello</div>;
311
+ }
312
+
313
+ act(() => {
314
+ ReactDOMClient.createRoot(container).render(<App />);
315
+ });
316
+
317
+ const result = getComponentTree(20, 'r9999');
318
+ expect(result.error).toMatch(/Component not found/);
319
+ });
320
+
321
+ it('assigns stable uids across calls', () => {
322
+ function App() {
323
+ return <div>hello</div>;
324
+ }
325
+
326
+ act(() => {
327
+ ReactDOMClient.createRoot(container).render(<App />);
328
+ });
329
+
330
+ const first = getComponentTree();
331
+ const second = getComponentTree();
332
+ expect(first).toEqual(second);
333
+ });
334
+
335
+ it('shows class components with class type', () => {
336
+ class MyComponent extends React.Component {
337
+ render() {
338
+ return <div>class</div>;
339
+ }
340
+ }
341
+
342
+ act(() => {
343
+ ReactDOMClient.createRoot(container).render(<MyComponent />);
344
+ });
345
+
346
+ const node = getComponentTree().find(n => n.name === 'MyComponent');
347
+ expect(node.type).toBe('class');
348
+ });
349
+
350
+ it('shows host components with host type', () => {
351
+ function App() {
352
+ return <div>hello</div>;
353
+ }
354
+
355
+ act(() => {
356
+ ReactDOMClient.createRoot(container).render(<App />);
357
+ });
358
+
359
+ const node = getComponentTree().find(n => n.name === 'div');
360
+ expect(node.type).toBe('host');
361
+ });
362
+
363
+ it('shows Memo components with memo type', () => {
364
+ function Inner() {
365
+ return <span>inner</span>;
366
+ }
367
+ const Memoized = React.memo(Inner);
368
+
369
+ act(() => {
370
+ ReactDOMClient.createRoot(container).render(<Memoized />);
371
+ });
372
+
373
+ const node = getComponentTree().find(n => n.name === 'Memo(Inner)');
374
+ expect(node).toBeDefined();
375
+ expect(node.type).toBe('memo');
376
+ });
377
+
378
+ it('shows ForwardRef components with forwardRef type', () => {
379
+ const FancyButton = React.forwardRef(function FancyButton(props, ref) {
380
+ return <button ref={ref}>{props.children}</button>;
381
+ });
382
+
383
+ act(() => {
384
+ ReactDOMClient.createRoot(container).render(
385
+ <FancyButton>click</FancyButton>,
386
+ );
387
+ });
388
+
389
+ const node = getComponentTree().find(
390
+ n => n.name === 'ForwardRef(FancyButton)',
391
+ );
392
+ expect(node).toBeDefined();
393
+ expect(node.type).toBe('forwardRef');
394
+ });
395
+
396
+ it('includes Fragment in the tree', () => {
397
+ function A() {
398
+ return <span>a</span>;
399
+ }
400
+ function B() {
401
+ return <span>b</span>;
402
+ }
403
+ function App() {
404
+ // Keyed Fragment creates a Fragment fiber
405
+ return (
406
+ <div>
407
+ <React.Fragment key="group">
408
+ <A />
409
+ <B />
410
+ </React.Fragment>
411
+ </div>
412
+ );
413
+ }
414
+
415
+ act(() => {
416
+ ReactDOMClient.createRoot(container).render(<App />);
417
+ });
418
+
419
+ const nodes = getComponentTree();
420
+ const fragment = nodes.find(n => n.type === 'fragment');
421
+ const a = nodes.find(n => n.name === 'A');
422
+ const b = nodes.find(n => n.name === 'B');
423
+ // The keyed Fragment is the div's child and parents A then B.
424
+ expect(fragment).toEqual({
425
+ uid: 'r3',
426
+ type: 'fragment',
427
+ name: 'Fragment',
428
+ key: 'group',
429
+ firstChild: a.uid,
430
+ nextSibling: null,
431
+ });
432
+ expect(a).toEqual({
433
+ uid: 'r4',
434
+ type: 'function',
435
+ name: 'A',
436
+ key: null,
437
+ firstChild: 'r5',
438
+ nextSibling: b.uid,
439
+ });
440
+ expect(b).toEqual({
441
+ uid: 'r6',
442
+ type: 'function',
443
+ name: 'B',
444
+ key: null,
445
+ firstChild: 'r7',
446
+ nextSibling: null,
447
+ });
448
+ });
449
+
450
+ it('includes HostRoot with type root', () => {
451
+ function App() {
452
+ return <div>hello</div>;
453
+ }
454
+
455
+ act(() => {
456
+ ReactDOMClient.createRoot(container).render(<App />);
457
+ });
458
+
459
+ const nodes = getComponentTree();
460
+ const root = nodes.find(n => n.type === 'root');
461
+ const app = nodes.find(n => n.name === 'App');
462
+ // The HostRoot is the tree's entry; its only child is App.
463
+ expect(root).toEqual({
464
+ uid: 'r1',
465
+ type: 'root',
466
+ name: 'createRoot()',
467
+ key: null,
468
+ firstChild: app.uid,
469
+ nextSibling: null,
470
+ });
471
+ });
472
+
473
+ it('includes Suspense in the tree', () => {
474
+ function App() {
475
+ return (
476
+ <React.Suspense fallback={<div>loading</div>}>
477
+ <div>content</div>
478
+ </React.Suspense>
479
+ );
480
+ }
481
+
482
+ act(() => {
483
+ ReactDOMClient.createRoot(container).render(<App />);
484
+ });
485
+
486
+ const suspense = getComponentTree().find(n => n.type === 'suspense');
487
+ // Suspense wraps its content via an internal primary/Offscreen child, so
488
+ // firstChild is a valid uid but its exact identity is an internal detail.
489
+ expect(suspense).toEqual({
490
+ uid: 'r2',
491
+ type: 'suspense',
492
+ name: 'Suspense',
493
+ key: null,
494
+ firstChild: 'r3',
495
+ nextSibling: null,
496
+ });
497
+ });
498
+
499
+ it('includes Context Provider in the tree', () => {
500
+ const MyContext = React.createContext('default');
501
+ function App() {
502
+ return (
503
+ <MyContext value="test">
504
+ <div>child</div>
505
+ </MyContext>
506
+ );
507
+ }
508
+
509
+ act(() => {
510
+ ReactDOMClient.createRoot(container).render(<App />);
511
+ });
512
+
513
+ const provider = getComponentTree().find(n => n.type === 'context');
514
+ expect(provider).toEqual({
515
+ uid: 'r2',
516
+ type: 'context',
517
+ name: 'Context.Provider',
518
+ key: null,
519
+ firstChild: 'r3',
520
+ nextSibling: null,
521
+ });
522
+ });
523
+
524
+ it('uids survive re-renders via alternate fiber handling', () => {
525
+ function Counter({count}) {
526
+ return <div>{'Count: ' + count}</div>;
527
+ }
528
+
529
+ const root = ReactDOMClient.createRoot(container);
530
+ act(() => {
531
+ root.render(<Counter count={0} />);
532
+ });
533
+
534
+ const counter1 = getComponentTree().find(n => n.name === 'Counter');
535
+ expect(counter1).toBeDefined();
536
+
537
+ act(() => {
538
+ root.render(<Counter count={1} />);
539
+ });
540
+
541
+ const counter2 = getComponentTree().find(n => n.name === 'Counter');
542
+ expect(counter2).toBeDefined();
543
+ // Same uid after re-render
544
+ expect(counter2.uid).toBe(counter1.uid);
545
+ });
546
+
547
+ it('removes unmounted roots from the tree', () => {
548
+ function App() {
549
+ return <div>hello</div>;
550
+ }
551
+
552
+ const root = ReactDOMClient.createRoot(container);
553
+ act(() => {
554
+ root.render(<App />);
555
+ });
556
+
557
+ const before = getComponentTree();
558
+ expect(before.find(n => n.name === 'App')).toBeDefined();
559
+
560
+ act(() => {
561
+ root.unmount();
562
+ });
563
+
564
+ const after = getComponentTree();
565
+ expect(after.error).toMatch(/No mounted React roots found/);
566
+ });
567
+ });
568
+
569
+ describe('findComponents', () => {
570
+ let findComponents;
571
+ let getComponentTree;
572
+
573
+ beforeEach(() => {
574
+ const tools = createTools(facade);
575
+ findComponents = tools.findComponents;
576
+ getComponentTree = tools.getComponentTree;
577
+ });
578
+
579
+ it('finds components by name (case-insensitive substring match)', () => {
580
+ function Header() {
581
+ return <h1>title</h1>;
582
+ }
583
+ function Footer() {
584
+ return <footer>foot</footer>;
585
+ }
586
+ function App() {
587
+ return (
588
+ <div>
589
+ <Header />
590
+ <Footer />
591
+ </div>
592
+ );
593
+ }
594
+
595
+ act(() => {
596
+ ReactDOMClient.createRoot(container).render(<App />);
597
+ });
598
+
599
+ const result = findComponents('header');
600
+ expect(result.totalCount).toBe(1);
601
+ expect(result.results).toHaveLength(1);
602
+ expect(result.results[0].name).toBe('Header');
603
+ expect(result.results[0].type).toBe('function');
604
+ expect(result.results[0].uid).toBe('r0');
605
+ });
606
+
607
+ it('returns all matches when multiple components match', () => {
608
+ function Card() {
609
+ return <div>card</div>;
610
+ }
611
+ function App() {
612
+ return (
613
+ <div>
614
+ <Card key="a" />
615
+ <Card key="b" />
616
+ <Card key="c" />
617
+ </div>
618
+ );
619
+ }
620
+
621
+ act(() => {
622
+ ReactDOMClient.createRoot(container).render(<App />);
623
+ });
624
+
625
+ const result = findComponents('Card');
626
+ expect(result.totalCount).toBe(3);
627
+ expect(result.results.map(r => r.key)).toEqual(['a', 'b', 'c']);
628
+ });
629
+
630
+ it('returns empty results when no components match', () => {
631
+ function App() {
632
+ return <div>hello</div>;
633
+ }
634
+
635
+ act(() => {
636
+ ReactDOMClient.createRoot(container).render(<App />);
637
+ });
638
+
639
+ const result = findComponents('NonExistent');
640
+ expect(result.totalCount).toBe(0);
641
+ expect(result.results).toEqual([]);
642
+ expect(result.page).toBe(1);
643
+ expect(result.totalPages).toBe(1);
644
+ });
645
+
646
+ it('scopes search to subtree when rootUid is provided', () => {
647
+ function Badge() {
648
+ return <span>badge</span>;
649
+ }
650
+ function Sidebar() {
651
+ return <Badge />;
652
+ }
653
+ function Main() {
654
+ return <Badge />;
655
+ }
656
+ function App() {
657
+ return (
658
+ <div>
659
+ <Sidebar />
660
+ <Main />
661
+ </div>
662
+ );
663
+ }
664
+
665
+ act(() => {
666
+ ReactDOMClient.createRoot(container).render(<App />);
667
+ });
668
+
669
+ // Find Sidebar's uid
670
+ const sidebar = getComponentTree().find(n => n.name === 'Sidebar');
671
+ expect(sidebar).toBeDefined();
672
+
673
+ // Search for Badge only under Sidebar
674
+ const result = findComponents('Badge', sidebar.uid);
675
+ expect(result.totalCount).toBe(1);
676
+ expect(result.results[0].name).toBe('Badge');
677
+
678
+ // Without rootUid, should find both Badges
679
+ const allResult = findComponents('Badge');
680
+ expect(allResult.totalCount).toBe(2);
681
+ });
682
+
683
+ it('paginates results with default page size of 10', () => {
684
+ function Item() {
685
+ return <li>item</li>;
686
+ }
687
+ function App() {
688
+ const items = [];
689
+ for (let i = 0; i < 15; i++) {
690
+ items.push(<Item key={String(i)} />);
691
+ }
692
+ return <ul>{items}</ul>;
693
+ }
694
+
695
+ act(() => {
696
+ ReactDOMClient.createRoot(container).render(<App />);
697
+ });
698
+
699
+ const page1 = findComponents('Item');
700
+ expect(page1.totalCount).toBe(15);
701
+ expect(page1.page).toBe(1);
702
+ expect(page1.pageSize).toBe(10);
703
+ expect(page1.totalPages).toBe(2);
704
+ expect(page1.results).toHaveLength(10);
705
+
706
+ const page2 = findComponents('Item', undefined, 2);
707
+ expect(page2.page).toBe(2);
708
+ expect(page2.results).toHaveLength(5);
709
+ });
710
+
711
+ it('supports custom page size', () => {
712
+ function Item() {
713
+ return <li>item</li>;
714
+ }
715
+ function App() {
716
+ return (
717
+ <ul>
718
+ <Item key="0" />
719
+ <Item key="1" />
720
+ <Item key="2" />
721
+ <Item key="3" />
722
+ <Item key="4" />
723
+ </ul>
724
+ );
725
+ }
726
+
727
+ act(() => {
728
+ ReactDOMClient.createRoot(container).render(<App />);
729
+ });
730
+
731
+ const result = findComponents('Item', undefined, 1, 2);
732
+ expect(result.totalCount).toBe(5);
733
+ expect(result.pageSize).toBe(2);
734
+ expect(result.totalPages).toBe(3);
735
+ expect(result.results).toHaveLength(2);
736
+ expect(result.results[0].key).toBe('0');
737
+ expect(result.results[1].key).toBe('1');
738
+
739
+ const page3 = findComponents('Item', undefined, 3, 2);
740
+ expect(page3.results).toHaveLength(1);
741
+ expect(page3.results[0].key).toBe('4');
742
+ });
743
+
744
+ it('clamps page number to valid range', () => {
745
+ function App() {
746
+ return <div>hello</div>;
747
+ }
748
+
749
+ act(() => {
750
+ ReactDOMClient.createRoot(container).render(<App />);
751
+ });
752
+
753
+ // Page 0 should clamp to 1
754
+ const low = findComponents('div', undefined, 0);
755
+ expect(low.page).toBe(1);
756
+
757
+ // Page beyond total should clamp to last page
758
+ const high = findComponents('div', undefined, 999);
759
+ expect(high.page).toBe(1);
760
+ });
761
+
762
+ it('results have same shape as tree snapshot nodes', () => {
763
+ function Widget() {
764
+ return <span>w</span>;
765
+ }
766
+ function App() {
767
+ return <Widget />;
768
+ }
769
+
770
+ act(() => {
771
+ ReactDOMClient.createRoot(container).render(<App />);
772
+ });
773
+
774
+ const result = findComponents('Widget');
775
+ expect(result.results).toHaveLength(1);
776
+ expect(result.results[0]).toEqual({
777
+ uid: 'r0',
778
+ type: 'function',
779
+ name: 'Widget',
780
+ key: null,
781
+ firstChild: 'r1',
782
+ nextSibling: null,
783
+ });
784
+ });
785
+
786
+ it('uids are consistent with getComponentTree', () => {
787
+ function Target() {
788
+ return <div>target</div>;
789
+ }
790
+ function App() {
791
+ return <Target />;
792
+ }
793
+
794
+ act(() => {
795
+ ReactDOMClient.createRoot(container).render(<App />);
796
+ });
797
+
798
+ // Get uid from tree snapshot
799
+ const target = getComponentTree().find(n => n.name === 'Target');
800
+ expect(target).toBeDefined();
801
+
802
+ // findComponents should return the same uid
803
+ const result = findComponents('Target');
804
+ expect(result.results[0].uid).toBe(target.uid);
805
+ });
806
+
807
+ it('matches host components by tag name', () => {
808
+ function App() {
809
+ return (
810
+ <div>
811
+ <span>a</span>
812
+ <span>b</span>
813
+ </div>
814
+ );
815
+ }
816
+
817
+ act(() => {
818
+ ReactDOMClient.createRoot(container).render(<App />);
819
+ });
820
+
821
+ const result = findComponents('span');
822
+ expect(result.totalCount).toBe(2);
823
+ expect(result.results[0].type).toBe('host');
824
+ expect(result.results[0].name).toBe('span');
825
+ });
826
+
827
+ it('does not match internal nodes with null displayName', () => {
828
+ function App() {
829
+ return (
830
+ <React.Fragment>
831
+ <div>hello</div>
832
+ </React.Fragment>
833
+ );
834
+ }
835
+
836
+ act(() => {
837
+ ReactDOMClient.createRoot(container).render(<App />);
838
+ });
839
+
840
+ // Fragment has null displayName in getDisplayNameForFiber,
841
+ // so it should not appear in search results
842
+ const fragmentResult = findComponents('Fragment');
843
+ expect(fragmentResult.totalCount).toBe(0);
844
+ });
845
+
846
+ it('finds Memo components by wrapped display name', () => {
847
+ function Inner() {
848
+ return <span>inner</span>;
849
+ }
850
+ const Memoized = React.memo(Inner);
851
+ function App() {
852
+ return <Memoized />;
853
+ }
854
+
855
+ act(() => {
856
+ ReactDOMClient.createRoot(container).render(<App />);
857
+ });
858
+
859
+ // memo(Inner) renders Inner inline (no separate FunctionComponent fiber),
860
+ // so the only match for "Inner" is the memo wrapper "Memo(Inner)".
861
+ const result = findComponents('Inner');
862
+ expect(result.totalCount).toBe(1);
863
+ expect(result.results).toHaveLength(1);
864
+ expect(result.results[0]).toEqual({
865
+ uid: 'r0',
866
+ type: 'memo',
867
+ name: 'Memo(Inner)',
868
+ key: null,
869
+ firstChild: 'r1',
870
+ nextSibling: null,
871
+ });
872
+ });
873
+
874
+ it('returns error for non-existent rootUid in scoped search', () => {
875
+ function App() {
876
+ return <div>hello</div>;
877
+ }
878
+
879
+ act(() => {
880
+ ReactDOMClient.createRoot(container).render(<App />);
881
+ });
882
+
883
+ const result = findComponents('App', 'r9999');
884
+ expect(result.error).toMatch(/Component not found/);
885
+ });
886
+ });
887
+
888
+ describe('getComponentSource', () => {
889
+ let getComponentSource;
890
+ let getComponentTree;
891
+
892
+ beforeEach(() => {
893
+ const tools = createTools(facade);
894
+ getComponentSource = tools.getComponentSource;
895
+ getComponentTree = tools.getComponentTree;
896
+ });
897
+
898
+ it('returns {source: null} for a function component when the location is unavailable', () => {
899
+ // The throwing trick that resolves a component's definition location does
900
+ // not produce file positions under jsdom, so source is null here. In a
901
+ // real browser this returns {name, fileName, line, column}.
902
+ function Greeting() {
903
+ return <div>Hello</div>;
904
+ }
905
+
906
+ act(() => {
907
+ ReactDOMClient.createRoot(container).render(<Greeting />);
908
+ });
909
+
910
+ const greeting = getComponentTree().find(n => n.name === 'Greeting');
911
+ expect(greeting).toBeDefined();
912
+ expect(getComponentSource(greeting.uid)).toEqual({source: null});
913
+ });
914
+
915
+ it('returns {source: null} for host components', () => {
916
+ function App() {
917
+ return <div>hello</div>;
918
+ }
919
+
920
+ act(() => {
921
+ ReactDOMClient.createRoot(container).render(<App />);
922
+ });
923
+
924
+ const div = getComponentTree().find(n => n.name === 'div');
925
+ expect(div).toBeDefined();
926
+ // Host components like div have no source location.
927
+ expect(getComponentSource(div.uid)).toEqual({source: null});
928
+ });
929
+
930
+ it('returns error for non-existent uid', () => {
931
+ const result = getComponentSource('r9999');
932
+ expect(result.error).toMatch(/Component not found/);
933
+ });
934
+ });
935
+
936
+ describe('getOwnersStack', () => {
937
+ let getOwnersStack;
938
+ let getComponentTree;
939
+
940
+ beforeEach(() => {
941
+ const tools = createTools(facade);
942
+ getOwnersStack = tools.getOwnersStack;
943
+ getComponentTree = tools.getComponentTree;
944
+ });
945
+
946
+ it('returns a stack string for a nested component', () => {
947
+ function Child() {
948
+ return <span>leaf</span>;
949
+ }
950
+ function Parent() {
951
+ return <Child />;
952
+ }
953
+ function App() {
954
+ return <Parent />;
955
+ }
956
+
957
+ act(() => {
958
+ ReactDOMClient.createRoot(container).render(<App />);
959
+ });
960
+
961
+ const child = getComponentTree().find(n => n.name === 'Child');
962
+ expect(child).toBeDefined();
963
+
964
+ const result = getOwnersStack(child.uid);
965
+ expect(typeof result.stack).toBe('string');
966
+ // The stack should mention the owner components
967
+ expect(result.stack).toContain('Parent');
968
+ expect(result.stack).toContain('App');
969
+ });
970
+
971
+ it('returns a stack string for the root component', () => {
972
+ function App() {
973
+ return <div>hello</div>;
974
+ }
975
+
976
+ act(() => {
977
+ ReactDOMClient.createRoot(container).render(<App />);
978
+ });
979
+
980
+ const app = getComponentTree().find(n => n.name === 'App');
981
+ const result = getOwnersStack(app.uid);
982
+ expect(typeof result.stack).toBe('string');
983
+ });
984
+
985
+ it('returns error for non-existent uid', () => {
986
+ const result = getOwnersStack('r9999');
987
+ expect(result.error).toMatch(/Component not found/);
988
+ });
989
+ });
990
+
991
+ describe('getOwnersBranch', () => {
992
+ let getOwnersBranch;
993
+ let getComponentTree;
994
+
995
+ beforeEach(() => {
996
+ const tools = createTools(facade);
997
+ getOwnersBranch = tools.getOwnersBranch;
998
+ getComponentTree = tools.getComponentTree;
999
+ });
1000
+
1001
+ it('returns owner list for a nested component', () => {
1002
+ function Child() {
1003
+ return <span>leaf</span>;
1004
+ }
1005
+ function Parent() {
1006
+ return <Child />;
1007
+ }
1008
+ function App() {
1009
+ return <Parent />;
1010
+ }
1011
+
1012
+ act(() => {
1013
+ ReactDOMClient.createRoot(container).render(<App />);
1014
+ });
1015
+
1016
+ const child = getComponentTree().find(n => n.name === 'Child');
1017
+ expect(child).toBeDefined();
1018
+
1019
+ const owners = getOwnersBranch(child.uid);
1020
+ expect(owners).toEqual([
1021
+ {
1022
+ uid: 'r2',
1023
+ name: 'Parent',
1024
+ type: 'function',
1025
+ },
1026
+ {
1027
+ uid: 'r0',
1028
+ name: 'App',
1029
+ type: 'function',
1030
+ },
1031
+ ]);
1032
+ });
1033
+
1034
+ it('each entry has uid, name, and type', () => {
1035
+ function Child() {
1036
+ return <span>leaf</span>;
1037
+ }
1038
+ function App() {
1039
+ return <Child />;
1040
+ }
1041
+
1042
+ act(() => {
1043
+ ReactDOMClient.createRoot(container).render(<App />);
1044
+ });
1045
+
1046
+ const child = getComponentTree().find(n => n.name === 'Child');
1047
+ const owners = getOwnersBranch(child.uid);
1048
+
1049
+ expect(owners).toHaveLength(1);
1050
+ expect(owners[0].uid).toBe('r0');
1051
+ expect(owners[0].name).toBe('App');
1052
+ expect(owners[0].type).toBe('function');
1053
+ });
1054
+
1055
+ it('owner uids are consistent with getComponentTree', () => {
1056
+ function Child() {
1057
+ return <span>leaf</span>;
1058
+ }
1059
+ function App() {
1060
+ return <Child />;
1061
+ }
1062
+
1063
+ act(() => {
1064
+ ReactDOMClient.createRoot(container).render(<App />);
1065
+ });
1066
+
1067
+ const tree = getComponentTree();
1068
+ const child = tree.find(n => n.name === 'Child');
1069
+ const app = tree.find(n => n.name === 'App');
1070
+
1071
+ const owners = getOwnersBranch(child.uid);
1072
+ expect(owners[0].uid).toBe(app.uid);
1073
+ });
1074
+
1075
+ it('returns empty array for root component with no owner', () => {
1076
+ function App() {
1077
+ return <div>hello</div>;
1078
+ }
1079
+
1080
+ act(() => {
1081
+ ReactDOMClient.createRoot(container).render(<App />);
1082
+ });
1083
+
1084
+ const app = getComponentTree().find(n => n.name === 'App');
1085
+ const owners = getOwnersBranch(app.uid);
1086
+ expect(owners).toEqual([]);
1087
+ });
1088
+
1089
+ it('returns error for non-existent uid', () => {
1090
+ const result = getOwnersBranch('r9999');
1091
+ expect(result.error).toMatch(/Component not found/);
1092
+ });
1093
+
1094
+ it('is ordered from immediate owner to root ancestor', () => {
1095
+ function GrandChild() {
1096
+ return <span>gc</span>;
1097
+ }
1098
+ function Child() {
1099
+ return <GrandChild />;
1100
+ }
1101
+ function Parent() {
1102
+ return <Child />;
1103
+ }
1104
+ function App() {
1105
+ return <Parent />;
1106
+ }
1107
+
1108
+ act(() => {
1109
+ ReactDOMClient.createRoot(container).render(<App />);
1110
+ });
1111
+
1112
+ const gc = getComponentTree().find(n => n.name === 'GrandChild');
1113
+ const owners = getOwnersBranch(gc.uid);
1114
+ expect(owners).toEqual([
1115
+ {
1116
+ uid: 'r3',
1117
+ name: 'Child',
1118
+ type: 'function',
1119
+ },
1120
+ {
1121
+ uid: 'r2',
1122
+ name: 'Parent',
1123
+ type: 'function',
1124
+ },
1125
+ {
1126
+ uid: 'r0',
1127
+ name: 'App',
1128
+ type: 'function',
1129
+ },
1130
+ ]);
1131
+ });
1132
+ });
1133
+
1134
+ describe('getComponentByUid', () => {
1135
+ let getComponentTree;
1136
+ let getComponentByUid;
1137
+
1138
+ beforeEach(() => {
1139
+ const tools = createTools(facade);
1140
+ getComponentTree = tools.getComponentTree;
1141
+ getComponentByUid = tools.getComponentByUid;
1142
+ });
1143
+
1144
+ it('returns error for non-existent uid', () => {
1145
+ const result = getComponentByUid('r9999');
1146
+ expect(result.error).toMatch(/Component not found/);
1147
+ });
1148
+
1149
+ it('returns info for a function component', () => {
1150
+ function Greeting() {
1151
+ return <div>Hello</div>;
1152
+ }
1153
+
1154
+ act(() => {
1155
+ ReactDOMClient.createRoot(container).render(<Greeting />);
1156
+ });
1157
+
1158
+ const greeting = getComponentTree().find(n => n.name === 'Greeting');
1159
+ expect(greeting).toBeDefined();
1160
+ const info = getComponentByUid(greeting.uid);
1161
+
1162
+ expect(info.uid).toBe(greeting.uid);
1163
+ expect(info.type).toBe('function');
1164
+ expect(info.name).toBe('Greeting');
1165
+ });
1166
+
1167
+ it('returns props (excluding children)', () => {
1168
+ function Button() {
1169
+ return <button>click</button>;
1170
+ }
1171
+
1172
+ act(() => {
1173
+ ReactDOMClient.createRoot(container).render(
1174
+ <Button text="Click me" disabled={true} />,
1175
+ );
1176
+ });
1177
+
1178
+ const button = getComponentTree().find(n => n.name === 'Button');
1179
+ const info = getComponentByUid(button.uid);
1180
+
1181
+ expect(info.props.text).toBe('Click me');
1182
+ expect(info.props.disabled).toBe(true);
1183
+ expect(info.props).not.toHaveProperty('children');
1184
+ });
1185
+
1186
+ it('serializes function props as descriptive strings', () => {
1187
+ function Button() {
1188
+ return <button>click</button>;
1189
+ }
1190
+
1191
+ function handleClick() {}
1192
+
1193
+ act(() => {
1194
+ ReactDOMClient.createRoot(container).render(
1195
+ <Button onClick={handleClick} />,
1196
+ );
1197
+ });
1198
+
1199
+ const button = getComponentTree().find(n => n.name === 'Button');
1200
+ const info = getComponentByUid(button.uid);
1201
+
1202
+ expect(info.props.onClick).toBe('[fn handleClick]');
1203
+ });
1204
+
1205
+ it('returns key when present', () => {
1206
+ function Item() {
1207
+ return <li>item</li>;
1208
+ }
1209
+ function List() {
1210
+ return (
1211
+ <ul>
1212
+ <Item key="first" />
1213
+ </ul>
1214
+ );
1215
+ }
1216
+
1217
+ act(() => {
1218
+ ReactDOMClient.createRoot(container).render(<List />);
1219
+ });
1220
+
1221
+ const item = getComponentTree().find(n => n.name === 'Item');
1222
+ const info = getComponentByUid(item.uid);
1223
+
1224
+ expect(info.key).toBe('first');
1225
+ });
1226
+
1227
+ it('returns correct type for class components', () => {
1228
+ class MyClass extends React.Component {
1229
+ render() {
1230
+ return <div>class</div>;
1231
+ }
1232
+ }
1233
+
1234
+ act(() => {
1235
+ ReactDOMClient.createRoot(container).render(<MyClass />);
1236
+ });
1237
+
1238
+ const myClass = getComponentTree().find(n => n.name === 'MyClass');
1239
+ expect(myClass).toBeDefined();
1240
+ const info = getComponentByUid(myClass.uid);
1241
+
1242
+ expect(info.type).toBe('class');
1243
+ expect(info.name).toBe('MyClass');
1244
+ });
1245
+
1246
+ it('returns correct type for host components', () => {
1247
+ function App() {
1248
+ return <div className="app" id="root" />;
1249
+ }
1250
+
1251
+ act(() => {
1252
+ ReactDOMClient.createRoot(container).render(<App />);
1253
+ });
1254
+
1255
+ const div = getComponentTree().find(n => n.name === 'div');
1256
+ const info = getComponentByUid(div.uid);
1257
+
1258
+ expect(info.type).toBe('host');
1259
+ expect(info.name).toBe('div');
1260
+ expect(info.props.className).toBe('app');
1261
+ expect(info.props.id).toBe('root');
1262
+ });
1263
+
1264
+ it('uses uids consistent with getComponentTree', () => {
1265
+ function Header() {
1266
+ return <h1>title</h1>;
1267
+ }
1268
+ function Footer() {
1269
+ return <footer>foot</footer>;
1270
+ }
1271
+ function App() {
1272
+ return (
1273
+ <div>
1274
+ <Header />
1275
+ <Footer />
1276
+ </div>
1277
+ );
1278
+ }
1279
+
1280
+ act(() => {
1281
+ ReactDOMClient.createRoot(container).render(<App />);
1282
+ });
1283
+
1284
+ const nodes = getComponentTree();
1285
+ nodes.forEach(node => {
1286
+ const info = getComponentByUid(node.uid);
1287
+ expect(info.uid).toBe(node.uid);
1288
+ });
1289
+ });
1290
+
1291
+ it('normalizes nested objects and arrays in props', () => {
1292
+ function Config() {
1293
+ return <div>config</div>;
1294
+ }
1295
+
1296
+ act(() => {
1297
+ ReactDOMClient.createRoot(container).render(
1298
+ <Config style={{color: 'red', fontSize: 14}} items={[1, 2, 3]} />,
1299
+ );
1300
+ });
1301
+
1302
+ const config = getComponentTree().find(n => n.name === 'Config');
1303
+ const info = getComponentByUid(config.uid);
1304
+ expect(info.props.style).toEqual({color: 'red', fontSize: 14});
1305
+ expect(info.props.items).toEqual([1, 2, 3]);
1306
+ });
1307
+
1308
+ it('normalizes symbol and undefined props', () => {
1309
+ function Widget() {
1310
+ return <div>w</div>;
1311
+ }
1312
+
1313
+ act(() => {
1314
+ ReactDOMClient.createRoot(container).render(
1315
+ <Widget sym={Symbol('test')} undef={undefined} />,
1316
+ );
1317
+ });
1318
+
1319
+ const widget = getComponentTree().find(n => n.name === 'Widget');
1320
+ const info = getComponentByUid(widget.uid);
1321
+ expect(info.props.sym).toBe('[symbol]');
1322
+ expect(info.props.undef).toBe(null);
1323
+ });
1324
+
1325
+ it('returns info for Memo component with correct type', () => {
1326
+ function Inner() {
1327
+ return <span>inner</span>;
1328
+ }
1329
+ const Memoized = React.memo(Inner);
1330
+
1331
+ act(() => {
1332
+ ReactDOMClient.createRoot(container).render(<Memoized value={42} />);
1333
+ });
1334
+
1335
+ const memo = getComponentTree().find(n => n.type === 'memo');
1336
+ expect(memo).toBeDefined();
1337
+ const info = getComponentByUid(memo.uid);
1338
+ expect(info.type).toBe('memo');
1339
+ });
1340
+
1341
+ it('returns info for ForwardRef component with correct type', () => {
1342
+ const FancyInput = React.forwardRef(function FancyInput(props, ref) {
1343
+ return <input ref={ref} />;
1344
+ });
1345
+
1346
+ act(() => {
1347
+ ReactDOMClient.createRoot(container).render(<FancyInput />);
1348
+ });
1349
+
1350
+ const fwd = getComponentTree().find(n => n.type === 'forwardRef');
1351
+ expect(fwd).toBeDefined();
1352
+ const info = getComponentByUid(fwd.uid);
1353
+ expect(info.type).toBe('forwardRef');
1354
+ });
1355
+
1356
+ it('returns no props when component has only children', () => {
1357
+ function Wrapper() {
1358
+ return <div>child</div>;
1359
+ }
1360
+
1361
+ act(() => {
1362
+ ReactDOMClient.createRoot(container).render(<Wrapper />);
1363
+ });
1364
+
1365
+ const wrapper = getComponentTree().find(n => n.name === 'Wrapper');
1366
+ const info = getComponentByUid(wrapper.uid);
1367
+ // No props key at all (children are excluded)
1368
+ expect(info.props).toBeUndefined();
1369
+ });
1370
+
1371
+ it('handles circular references in props without stack overflow', () => {
1372
+ function Widget() {
1373
+ return <div>widget</div>;
1374
+ }
1375
+
1376
+ const circular = {a: 1};
1377
+ circular.self = circular;
1378
+
1379
+ act(() => {
1380
+ ReactDOMClient.createRoot(container).render(<Widget data={circular} />);
1381
+ });
1382
+
1383
+ const widget = getComponentTree().find(n => n.name === 'Widget');
1384
+ // Should not throw or stack overflow
1385
+ const info = getComponentByUid(widget.uid);
1386
+ expect(info.props.data.a).toBe(1);
1387
+ expect(info.props.data.self).toBe('[circular]');
1388
+ });
1389
+
1390
+ it('handles deeply nested objects in props without stack overflow', () => {
1391
+ function Widget() {
1392
+ return <div>widget</div>;
1393
+ }
1394
+
1395
+ // Create a very deeply nested object
1396
+ let deep = {value: 'leaf'};
1397
+ for (let i = 0; i < 200; i++) {
1398
+ deep = {nested: deep};
1399
+ }
1400
+
1401
+ act(() => {
1402
+ ReactDOMClient.createRoot(container).render(<Widget data={deep} />);
1403
+ });
1404
+
1405
+ const widget = getComponentTree().find(n => n.name === 'Widget');
1406
+ // Should not throw or stack overflow
1407
+ const info = getComponentByUid(widget.uid);
1408
+ expect(info.props.data).toBeDefined();
1409
+ });
1410
+
1411
+ it('returns the full hooks tree for a function component', () => {
1412
+ function useCounter() {
1413
+ const [c] = React.useState(0);
1414
+ return c;
1415
+ }
1416
+ function Widget() {
1417
+ const [count] = React.useState(7);
1418
+ React.useEffect(() => {}, []);
1419
+ const [obj] = React.useState({color: 'red'});
1420
+ useCounter();
1421
+ const ref = React.useRef(1);
1422
+ const memo = React.useMemo(() => 5, []);
1423
+ return (
1424
+ <div>
1425
+ {count}
1426
+ {obj.color}
1427
+ {ref.current}
1428
+ {memo}
1429
+ </div>
1430
+ );
1431
+ }
1432
+
1433
+ act(() => {
1434
+ ReactDOMClient.createRoot(container).render(<Widget />);
1435
+ });
1436
+
1437
+ const widget = getComponentTree().find(n => n.name === 'Widget');
1438
+ const info = getComponentByUid(widget.uid);
1439
+
1440
+ // Full structural assertion: every hook node, in order, with its id
1441
+ // (sequential per primitive hook; custom hooks are null), name, normalized
1442
+ // value (the Effect's create fn becomes '[fn]'), and subHooks.
1443
+ expect(info.hooks).toEqual([
1444
+ {id: 0, name: 'State', value: 7, subHooks: []},
1445
+ {id: 1, name: 'Effect', value: '[fn]', subHooks: []},
1446
+ {id: 2, name: 'State', value: {color: 'red'}, subHooks: []},
1447
+ {
1448
+ id: null,
1449
+ name: 'Counter',
1450
+ value: null,
1451
+ subHooks: [{id: 3, name: 'State', value: 0, subHooks: []}],
1452
+ },
1453
+ {id: 4, name: 'Ref', value: 1, subHooks: []},
1454
+ {id: 5, name: 'Memo', value: 5, subHooks: []},
1455
+ ]);
1456
+ });
1457
+
1458
+ it('captures the useContext hook with its provided value', () => {
1459
+ const ThemeContext = React.createContext('light');
1460
+ function Themed() {
1461
+ const theme = React.useContext(ThemeContext);
1462
+ const [count] = React.useState(0);
1463
+ return (
1464
+ <div>
1465
+ {theme}
1466
+ {count}
1467
+ </div>
1468
+ );
1469
+ }
1470
+ function App() {
1471
+ return (
1472
+ <ThemeContext value="dark">
1473
+ <Themed />
1474
+ </ThemeContext>
1475
+ );
1476
+ }
1477
+
1478
+ act(() => {
1479
+ ReactDOMClient.createRoot(container).render(<App />);
1480
+ });
1481
+
1482
+ const themed = getComponentTree().find(n => n.name === 'Themed');
1483
+ const info = getComponentByUid(themed.uid);
1484
+ // useContext is captured as a "Context" hook holding the provider's value.
1485
+ // It does not consume a primitive hook slot, so its id is null; the
1486
+ // following useState is the first primitive hook (id 0).
1487
+ expect(info.hooks).toEqual([
1488
+ {id: null, name: 'Context', value: 'dark', subHooks: []},
1489
+ {id: 0, name: 'State', value: 0, subHooks: []},
1490
+ ]);
1491
+ });
1492
+
1493
+ it('returns an empty hooks array for a function component with no hooks', () => {
1494
+ function Plain() {
1495
+ return <div>plain</div>;
1496
+ }
1497
+
1498
+ act(() => {
1499
+ ReactDOMClient.createRoot(container).render(<Plain />);
1500
+ });
1501
+
1502
+ const plain = getComponentTree().find(n => n.name === 'Plain');
1503
+ const info = getComponentByUid(plain.uid);
1504
+ expect(info.hooks).toEqual([]);
1505
+ });
1506
+
1507
+ it('does not include hooks for class components', () => {
1508
+ class MyClass extends React.Component {
1509
+ render() {
1510
+ return <div>class</div>;
1511
+ }
1512
+ }
1513
+
1514
+ act(() => {
1515
+ ReactDOMClient.createRoot(container).render(<MyClass />);
1516
+ });
1517
+
1518
+ const myClass = getComponentTree().find(n => n.name === 'MyClass');
1519
+ const info = getComponentByUid(myClass.uid);
1520
+ expect(info.hooks).toBeUndefined();
1521
+ });
1522
+
1523
+ it('does not include hooks for host components', () => {
1524
+ function App() {
1525
+ return <div>hello</div>;
1526
+ }
1527
+
1528
+ act(() => {
1529
+ ReactDOMClient.createRoot(container).render(<App />);
1530
+ });
1531
+
1532
+ const div = getComponentTree().find(n => n.name === 'div');
1533
+ const info = getComponentByUid(div.uid);
1534
+ expect(info.hooks).toBeUndefined();
1535
+ });
1536
+ });
1537
});