/**
* 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 ;
}
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 ;
}
function Footer() {
return ;
}
function App() {
return (
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
// First, get the full tree to find Header's uid
const header = getComponentTree().find(n => n.name === 'Header');
expect(header).toBeDefined();
// Snapshot from Header
const sub = getComponentTree(20, header.uid);
const names = sub.map(n => n.name);
expect(names).toContain('Header');
expect(names).toContain('Nav');
// Should NOT contain App or Footer
expect(names).not.toContain('App');
expect(names).not.toContain('Footer');
});
it('returns error for non-existent rootUid', () => {
function App() {
return
hello
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const result = getComponentTree(20, 'r9999');
expect(result.error).toMatch(/Component not found/);
});
it('assigns stable uids across calls', () => {
function App() {
return
hello
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const first = getComponentTree();
const second = getComponentTree();
expect(first).toEqual(second);
});
it('shows class components with class type', () => {
class MyComponent extends React.Component {
render() {
return
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
// Page 0 should clamp to 1
const low = findComponents('div', undefined, 0);
expect(low.page).toBe(1);
// Page beyond total should clamp to last page
const high = findComponents('div', undefined, 999);
expect(high.page).toBe(1);
});
it('results have same shape as tree snapshot nodes', () => {
function Widget() {
return w;
}
function App() {
return ;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const result = findComponents('Widget');
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
uid: 'r0',
type: 'function',
name: 'Widget',
key: null,
firstChild: 'r1',
nextSibling: null,
});
});
it('uids are consistent with getComponentTree', () => {
function Target() {
return
target
;
}
function App() {
return ;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
// Get uid from tree snapshot
const target = getComponentTree().find(n => n.name === 'Target');
expect(target).toBeDefined();
// findComponents should return the same uid
const result = findComponents('Target');
expect(result.results[0].uid).toBe(target.uid);
});
it('matches host components by tag name', () => {
function App() {
return (
ab
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const result = findComponents('span');
expect(result.totalCount).toBe(2);
expect(result.results[0].type).toBe('host');
expect(result.results[0].name).toBe('span');
});
it('does not match internal nodes with null displayName', () => {
function App() {
return (
hello
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
// Fragment has null displayName in getDisplayNameForFiber,
// so it should not appear in search results
const fragmentResult = findComponents('Fragment');
expect(fragmentResult.totalCount).toBe(0);
});
it('finds Memo components by wrapped display name', () => {
function Inner() {
return inner;
}
const Memoized = React.memo(Inner);
function App() {
return ;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
// memo(Inner) renders Inner inline (no separate FunctionComponent fiber),
// so the only match for "Inner" is the memo wrapper "Memo(Inner)".
const result = findComponents('Inner');
expect(result.totalCount).toBe(1);
expect(result.results).toHaveLength(1);
expect(result.results[0]).toEqual({
uid: 'r0',
type: 'memo',
name: 'Memo(Inner)',
key: null,
firstChild: 'r1',
nextSibling: null,
});
});
it('returns error for non-existent rootUid in scoped search', () => {
function App() {
return
hello
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const result = findComponents('App', 'r9999');
expect(result.error).toMatch(/Component not found/);
});
});
describe('getComponentSource', () => {
let getComponentSource;
let getComponentTree;
beforeEach(() => {
const tools = createTools(facade);
getComponentSource = tools.getComponentSource;
getComponentTree = tools.getComponentTree;
});
it('returns {source: null} for a function component when the location is unavailable', () => {
// The throwing trick that resolves a component's definition location does
// not produce file positions under jsdom, so source is null here. In a
// real browser this returns {name, fileName, line, column}.
function Greeting() {
return
);
}
function App() {
return (
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const themed = getComponentTree().find(n => n.name === 'Themed');
const info = getComponentByUid(themed.uid, true);
// useContext is captured as a "Context" hook holding the provider's value.
// It does not consume a primitive hook slot, so its id is null; the
// following useState is the first primitive hook (id 0).
expect(info.hooks).toEqual([
{id: null, name: 'Context', value: 'dark', subHooks: []},
{id: 0, name: 'State', value: 0, subHooks: []},
]);
});
it('returns an empty hooks array for a function component with no hooks', () => {
function Plain() {
return
plain
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const plain = getComponentTree().find(n => n.name === 'Plain');
const info = getComponentByUid(plain.uid, true);
expect(info.hooks).toEqual([]);
});
it('does not include hooks for class components', () => {
class MyClass extends React.Component {
render() {
return
class
;
}
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const myClass = getComponentTree().find(n => n.name === 'MyClass');
const info = getComponentByUid(myClass.uid, true);
expect(info.hooks).toBeUndefined();
});
it('does not include hooks for host components', () => {
function App() {
return
hello
;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const div = getComponentTree().find(n => n.name === 'div');
const info = getComponentByUid(div.uid, true);
expect(info.hooks).toBeUndefined();
});
});
describe('getComponentByHostInstance', () => {
let getComponentTree;
let getComponentByUid;
let getComponentByHostInstance;
beforeEach(() => {
const tools = createTools(facade);
getComponentTree = tools.getComponentTree;
getComponentByUid = tools.getComponentByUid;
getComponentByHostInstance = tools.getComponentByHostInstance;
});
it('returns the host component for a DOM host element', () => {
function Child({label}) {
return {label};
}
function App() {
return (
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const span = container.querySelector('span.leaf');
const host = getComponentTree().find(n => n.name === 'span');
const result = getComponentByHostInstance(span);
expect(result).toEqual(getComponentByUid(host.uid));
expect(result).toMatchObject({
uid: host.uid,
type: 'host',
name: 'span',
props: {className: 'leaf'},
});
});
it('returns the host component rather than the tree owner', () => {
function Wrapper({children}) {
return {children};
}
function App() {
return (
);
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const button = container.querySelector('button.action');
const tree = getComponentTree();
const host = tree.find(n => n.name === 'button');
const wrapper = tree.find(n => n.name === 'Wrapper');
const app = tree.find(n => n.name === 'App');
const result = getComponentByHostInstance(button);
expect(result.uid).toBe(host.uid);
expect(result.uid).not.toBe(wrapper.uid);
expect(result.uid).not.toBe(app.uid);
expect(result).toMatchObject({
type: 'host',
name: 'button',
props: {className: 'action'},
});
});
it('keeps uids stable across re-renders via alternate fibers', () => {
function Counter({count}) {
return
{'Count: ' + count}
;
}
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render();
});
const div = container.querySelector('div.counter');
const first = getComponentByHostInstance(div);
act(() => {
root.render();
});
const second = getComponentByHostInstance(div);
expect(second.uid).toBe(first.uid);
expect(second.name).toBe('div');
expect(second.type).toBe('host');
expect(second.props.className).toBe('counter');
});
it('does not walk platform parent pointers for unmanaged nested nodes', () => {
function App() {
return ;
}
act(() => {
ReactDOMClient.createRoot(container).render();
});
const host = container.querySelector('div.host');
const unmanagedChild = document.createElement('i');
host.appendChild(unmanagedChild);
expect(getComponentByHostInstance(unmanagedChild)).toEqual({
error: 'Host instance is not managed by React',
});
});
it('returns an error when no roots are mounted', () => {
expect(getComponentByHostInstance({})).toEqual({
error: 'No mounted React roots found',
});
});
it('returns an error for null or undefined references', () => {
expect(getComponentByHostInstance(null)).toEqual({
error: 'Host instance is required',
});
expect(getComponentByHostInstance(undefined)).toEqual({
error: 'Host instance is required',
});
});
});
describe('profiler', () => {
let startProfiling;
let stopProfiling;
let getTraceOverview;
let getCommitReport;
let getComponentTree;
let getComponentByUid;
beforeEach(() => {
const tools = createTools(facade);
startProfiling = tools.startProfiling;
stopProfiling = tools.stopProfiling;
getTraceOverview = tools.getTraceOverview;
getCommitReport = tools.getCommitReport;
getComponentTree = tools.getComponentTree;
getComponentByUid = tools.getComponentByUid;
});
it('startProfiling returns the started status and trace name', () => {
expect(startProfiling('my-trace')).toEqual({
status: 'started',
traceName: 'my-trace',
});
stopProfiling();
});
it('startProfiling auto-generates a trace name when none is provided', () => {
const result = startProfiling();
expect(result.status).toBe('started');
expect(result.traceName).toMatch(/^trace-\d+$/);
stopProfiling();
});
it('stopProfiling reports the trace name and commit count', () => {
startProfiling('test-trace');
expect(stopProfiling()).toEqual({
status: 'stopped',
traceName: 'test-trace',
commits: 0,
});
});
it('cannot start profiling twice', () => {
startProfiling('first');
expect(startProfiling('second')).toEqual({
error: 'Already profiling trace "first"',
});
stopProfiling();
});
it('cannot stop when not profiling', () => {
expect(stopProfiling()).toEqual({error: 'Not currently profiling'});
});
it('records one commit per render and reports the count on stop', () => {
function Counter({count}) {
return