[react-devtools-facade] 4/ multi-root and multi-renderer tests (#36599)
This PR just adds more tests that validate Facade's implementation against setups with multiple React roots or Renderers.
Ruslan Lesiutin committed
Jun 18, 2026 at 20:29 UTC
552037d01b1c18986f8809e095b63592f13d74b6
1 file changed
+155
-2
packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js
+155
-2
@@ -44,11 +44,9 @@ describe('react-devtools-facade', () => {
44
act = React.act;
45
46
container = document.createElement('div');
47
- document.body.appendChild(container);
47
});
48
49
afterEach(() => {
51
- document.body.removeChild(container);
50
container = null;
51
});
52
@@ -1891,4 +1889,159 @@ describe('react-devtools-facade', () => {
1889
}).not.toThrow();
1890
});
1891
});
1892
+
1893
+ describe('multiple roots and renderers', () => {
1894
+ it('inject() registers a new renderer and initializes its internals', () => {
1895
+ // react-dom registered itself as a renderer when it was required. Here we
1896
+ // register a second (simulated) renderer to cover the registration
1897
+ // contract; the cross-root tests below use a single react-dom renderer.
1898
+ const before = facade.hook.renderers.size;
1899
+ const id = facade.hook.inject({
1900
+ reconcilerVersion: '18.2.0',
1901
+ version: '18.2.0',
1902
+ });
1903
+ expect(typeof id).toBe('number');
1904
+ expect(facade.hook.renderers.size).toBe(before + 1);
1905
+ expect(facade.rendererInternals.has(id)).toBe(true);
1906
+ });
1907
+
1908
+ it('getComponentTree aggregates components from multiple roots', () => {
1909
+ function AppA() {
1910
+ return <div>A</div>;
1911
+ }
1912
+ function AppB() {
1913
+ return <div>B</div>;
1914
+ }
1915
+
1916
+ const containerB = document.createElement('div');
1917
+
1918
+ act(() => {
1919
+ ReactDOMClient.createRoot(container).render(<AppA />);
1920
+ ReactDOMClient.createRoot(containerB).render(<AppB />);
1921
+ });
1922
+
1923
+ const tree = createTools(facade).getComponentTree();
1924
+ // Two roots → exactly two HostRoot nodes.
1925
+ expect(tree.filter(n => n.type === 'root')).toHaveLength(2);
1926
+
1927
+ const appA = tree.find(n => n.name === 'AppA');
1928
+ const appB = tree.find(n => n.name === 'AppB');
1929
+ // Uids come from a per-call counter that spans every root, in root
1930
+ // order: root A gets r0–r2, root B gets r3–r5, so uids are
1931
+ // globally unique across roots.
1932
+ expect(appA).toEqual({
1933
+ uid: 'r0',
1934
+ type: 'function',
1935
+ name: 'AppA',
1936
+ key: null,
1937
+ firstChild: 'r2',
1938
+ nextSibling: null,
1939
+ });
1940
+ expect(appB).toEqual({
1941
+ uid: 'r3',
1942
+ type: 'function',
1943
+ name: 'AppB',
1944
+ key: null,
1945
+ firstChild: 'r5',
1946
+ nextSibling: null,
1947
+ });
1948
+ });
1949
+
1950
+ it('findComponents finds matches across multiple roots', () => {
1951
+ function Shared() {
1952
+ return <span>shared</span>;
1953
+ }
1954
+ function RootA() {
1955
+ return <Shared />;
1956
+ }
1957
+ function RootB() {
1958
+ return <Shared />;
1959
+ }
1960
+
1961
+ const containerB = document.createElement('div');
1962
+ act(() => {
1963
+ ReactDOMClient.createRoot(container).render(<RootA />);
1964
+ ReactDOMClient.createRoot(containerB).render(<RootB />);
1965
+ });
1966
+
1967
+ const result = createTools(facade).findComponents('Shared');
1968
+ expect(result.totalCount).toBe(2);
1969
+ expect(result.results.map(r => r.name)).toEqual(['Shared', 'Shared']);
1970
+ // Globally unique uids, assigned in result order across both roots.
1971
+ expect(result.results.map(r => r.uid)).toEqual(['r0', 'r2']);
1972
+ });
1973
+
1974
+ it('resolves uids from any root via getComponentByUid', () => {
1975
+ function Widget() {
1976
+ return <div>w</div>;
1977
+ }
1978
+ function RootA() {
1979
+ return <Widget />;
1980
+ }
1981
+ function RootB() {
1982
+ return <Widget />;
1983
+ }
1984
+
1985
+ const containerB = document.createElement('div');
1986
+ act(() => {
1987
+ ReactDOMClient.createRoot(container).render(<RootA />);
1988
+ ReactDOMClient.createRoot(containerB).render(<RootB />);
1989
+ });
1990
+
1991
+ const tools = createTools(facade);
1992
+ const widgets = tools.findComponents('Widget').results;
1993
+ expect(widgets).toHaveLength(2);
1994
+ widgets.forEach(w => {
1995
+ expect(tools.getComponentByUid(w.uid).name).toBe('Widget');
1996
+ });
1997
+ });
1998
+
1999
+ it('profiling records commits from all roots', () => {
2000
+ function CounterA({count}) {
2001
+ return <div>{'A:' + count}</div>;
2002
+ }
2003
+ function CounterB({count}) {
2004
+ return <div>{'B:' + count}</div>;
2005
+ }
2006
+
2007
+ const containerB = document.createElement('div');
2008
+
2009
+ const rootA = ReactDOMClient.createRoot(container);
2010
+ const rootB = ReactDOMClient.createRoot(containerB);
2011
+ act(() => {
2012
+ rootA.render(<CounterA count={0} />);
2013
+ rootB.render(<CounterB count={0} />);
2014
+ });
2015
+
2016
+ const tools = createTools(facade);
2017
+ tools.startProfiling('multi-root-trace');
2018
+ act(() => {
2019
+ rootA.render(<CounterA count={1} />);
2020
+ });
2021
+ act(() => {
2022
+ rootB.render(<CounterB count={1} />);
2023
+ });
2024
+
2025
+ expect(tools.stopProfiling()).toEqual({
2026
+ status: 'stopped',
2027
+ traceName: 'multi-root-trace',
2028
+ commits: 2,
2029
+ });
2030
+
2031
+ const overview = tools.getTraceOverview('multi-root-trace');
2032
+ expect(overview).toHaveLength(2);
2033
+ // Separate act() blocks force exactly one commit each, in order, so
2034
+ // commit 0 is rootA's re-render and commit 1 is rootB's.
2035
+ const names0 = tools
2036
+ .getCommitReport('multi-root-trace', 0)
2037
+ .components.map(c => c.name);
2038
+ const names1 = tools
2039
+ .getCommitReport('multi-root-trace', 1)
2040
+ .components.map(c => c.name);
2041
+ expect(names0).toContain('CounterA');
2042
+ expect(names0).not.toContain('CounterB');
2043
+ expect(names1).toContain('CounterB');
2044
+ expect(names1).not.toContain('CounterA');
2045
+ });
2046
+ });
2047
});