/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; let installFacade; let createTools; let facade; let React; let ReactDOMClient; let act; let container; // Profiler durations are timing-dependent: null when the build does not collect // them, otherwise a non-negative number. function isDuration(value) { return value === null || (typeof value === 'number' && value >= 0); } describe('react-devtools-facade', () => { beforeEach(() => { jest.resetModules(); global.IS_REACT_ACT_ENVIRONMENT = true; // The hook lives on globalThis, which jsdom shares across tests in this // file, so a leftover hook would make installFacade() below throw. Remove // it for a clean slate. (The facade never installs any other global, which // the "does not install any tool globals" test verifies.) delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__; // Install the facade BEFORE React so the hook captures the first commit. // Import through the package entry point to exercise the public surface. const facadeAPI = require('../../index'); installFacade = facadeAPI.installFacade; createTools = facadeAPI.createTools; facade = installFacade(); React = require('react'); ReactDOMClient = require('react-dom/client'); act = React.act; container = document.createElement('div'); }); afterEach(() => { jest.dontMock('react-debug-tools'); container = null; }); it('installs __REACT_DEVTOOLS_GLOBAL_HOOK__ on globalThis', () => { expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook); }); it('returns a Facade handle exposing the hook and tracked state', () => { expect(facade.hook).toBe(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__); expect(facade.fiberRoots).toBeInstanceOf(Map); expect(facade.rendererInternals).toBeInstanceOf(Map); expect(facade.profilingState).toEqual({ isActive: false, currentTraceName: null, traces: expect.any(Map), onCommit: null, onPostCommit: null, }); }); it('does not install any tool globals (the integrator decides those)', () => { expect(globalThis.__REACT_TOOLS__).toBeUndefined(); expect(globalThis.__REACT_LLM_TOOLS__).toBeUndefined(); }); it('attaches to an existing hook instead of installing a second one', () => { // A facade hook is already installed on globalThis (beforeEach). A second // installFacade() attaches to it rather than throwing or replacing it — this // is the path taken when the React DevTools extension is present. const attached = installFacade(); expect(attached.hook).toBe(facade.hook); expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook); }); it('an attached facade back-fills roots already tracked by the hook', () => { function App() { return
hi
; } act(() => { ReactDOMClient.createRoot(container).render(); }); // Attaching after the app mounted picks up the already-tracked root. const attached = installFacade(); const tree = createTools(attached).getComponentTree(); expect(tree.find(n => n.name === 'App')).toBeDefined(); }); it('an attached facade tracks later commits and profiles them', () => { function Counter({count}) { return
{'n:' + count}
; } const root = ReactDOMClient.createRoot(container); act(() => { root.render(); }); const tools = createTools(installFacade()); expect( tools.getComponentTree().find(n => n.name === 'Counter'), ).toBeDefined(); // A commit after attaching flows through the wrapped onCommitFiberRoot. tools.startProfiling('attached-trace'); act(() => { root.render(); }); expect(tools.stopProfiling()).toEqual({ status: 'stopped', traceName: 'attached-trace', commits: 1, }); }); it('installs onto an explicit target without touching globalThis', () => { const target = {}; const localFacade = installFacade(target); expect(target.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(localFacade.hook); // The explicit-target facade is fully independent of the global one. expect(localFacade.hook).not.toBe(facade.hook); expect(localFacade.fiberRoots).not.toBe(facade.fiberRoots); // ...and installing onto a target does not disturb the global hook. expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook); }); it('records the renderer and its fiber root on mount', () => { function Greeting() { return
Hello
; } act(() => { ReactDOMClient.createRoot(container).render(); }); // React injected a renderer: its internal constants were captured... expect(facade.rendererInternals.size).toBeGreaterThan(0); // ...and the hook recorded the committed root in facade.fiberRoots. let totalRoots = 0; facade.fiberRoots.forEach(roots => { totalRoots += roots.size; }); expect(totalRoots).toBeGreaterThan(0); }); it('removes unmounted roots from tracking', () => { function App() { return
hello
; } const root = ReactDOMClient.createRoot(container); act(() => { root.render(); }); const rendererID = Array.from(facade.hook.renderers.keys())[0]; expect(facade.hook.getFiberRoots(rendererID).size).toBeGreaterThan(0); act(() => { root.unmount(); }); expect(facade.hook.getFiberRoots(rendererID).size).toBe(0); }); describe('getComponentTree', () => { let getComponentTree; beforeEach(() => { getComponentTree = createTools(facade).getComponentTree; }); it('returns error when nothing is rendered', () => { const result = getComponentTree(); expect(result.error).toMatch(/No mounted React roots found/); }); it('returns an array of component nodes', () => { function App() { return
hello
; } act(() => { ReactDOMClient.createRoot(container).render(); }); const result = getComponentTree(); expect(Array.isArray(result)).toBe(true); const app = result.find(n => n.name === 'App'); const div = result.find(n => n.name === 'div'); // App is the root's only child; its child is the host div. expect(app).toEqual({ uid: 'r0', type: 'function', name: 'App', key: null, firstChild: div.uid, nextSibling: null, }); // A single string child ('hello') is stored as a prop, not a child fiber, // so the div is a leaf in the tree. expect(div).toEqual({ uid: 'r2', type: 'host', name: 'div', key: null, firstChild: null, nextSibling: null, }); }); it('encodes firstChild and nextSibling relationships', () => { function Header() { return

title

; } function Footer() { return
foot
; } function App() { return (
); } act(() => { ReactDOMClient.createRoot(container).render(); }); const nodes = getComponentTree(); const app = nodes.find(n => n.name === 'App'); const div = nodes.find(n => n.name === 'div'); const header = nodes.find(n => n.name === 'Header'); const footer = nodes.find(n => n.name === 'Footer'); // App's firstChild is div expect(app.firstChild).toBe(div.uid); // div's firstChild is Header expect(div.firstChild).toBe(header.uid); // Header's nextSibling is Footer expect(header.nextSibling).toBe(footer.uid); // Footer has no nextSibling expect(footer.nextSibling).toBe(null); }); it('shows keys in the output', () => { function Item() { return
  • item
  • ; } function List() { return (
    ); } act(() => { ReactDOMClient.createRoot(container).render(); }); const items = getComponentTree().filter(n => n.name === 'Item'); expect(items.map(i => i.key)).toEqual(['a', 'b']); }); it('limits depth with the depth parameter', () => { function Child() { return leaf; } function Parent() { return ; } function App() { return ; } act(() => { ReactDOMClient.createRoot(container).render(); }); const names = snapshot => snapshot.map(n => n.name); // depth=0: only the root node (HostRoot) const shallow = getComponentTree(0); expect(shallow).toHaveLength(1); expect(shallow[0].type).toBe('root'); // depth=1: root + App const d1 = getComponentTree(1); expect(names(d1)).toContain('App'); expect(names(d1)).not.toContain('Parent'); // depth=2: root + App + Parent const d2 = getComponentTree(2); expect(names(d2)).toContain('App'); expect(names(d2)).toContain('Parent'); expect(names(d2)).not.toContain('Child'); const deep = getComponentTree(20); expect(names(deep)).toEqual( expect.arrayContaining(['App', 'Parent', 'Child']), ); }); it('starts from a specific node when rootUid is provided', () => { function Nav() { return ; } function Header() { return