@samitouri / QOS-React / commits / 552037d01b

[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 });