/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-devtools-shared/src/__tests__/store-test.js
3 246 строк
97 KB
Sebastian "Sebbie" Silbermann
[DevTools] Exclude Suspense boundaries in hidden Activity (#34756)
14 окт 2025, 14:57
Не верифицирован
14 окт 2025, 14:57
73507ec
Код
Авторство
О чём код?
/** * 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. * * @flow */ import {getVersionedRenderImplementation} from './utils'; describe('Store', () => { let React; let ReactDOM; let ReactDOMClient; let agent; let act; let actAsync; let bridge; let createDisplayNameFilter; let getRendererID; let legacyRender; let previousComponentFilters; let store; let withErrorsOrWarningsIgnored; function readValue(promise) { if (typeof React.use === 'function') { return React.use(promise); } // Support for React < 19.0 switch (promise.status) { case 'fulfilled': return promise.value; case 'rejected': throw promise.reason; case 'pending': throw promise; default: promise.status = 'pending'; promise.then( value => { promise.status = 'fulfilled'; promise.value = value; }, reason => { promise.status = 'rejected'; promise.reason = reason; }, ); throw promise; } } beforeAll(() => { // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes Element.prototype.getClientRects = function (this: Element) { const textContent = this.textContent; return [ new DOMRect(1, 2, textContent.length, textContent.split('\n').length), ]; }; }); beforeEach(() => { global.IS_REACT_ACT_ENVIRONMENT = true; agent = global.agent; bridge = global.bridge; store = global.store; previousComponentFilters = store.componentFilters; React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); const utils = require('./utils'); act = utils.act; actAsync = utils.actAsync; getRendererID = utils.getRendererID; legacyRender = utils.legacyRender; createDisplayNameFilter = utils.createDisplayNameFilter; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; }); afterEach(() => { store.componentFilters = previousComponentFilters; }); const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 it('should not allow a root node to be collapsed', async () => { const Component = () => <div>Hi</div>; await act(() => render(<Component count={4} />)); expect(store).toMatchInlineSnapshot(` [root] <Component> `); expect(store.roots).toHaveLength(1); const rootID = store.roots[0]; expect(() => store.toggleIsCollapsed(rootID, true)).toThrow( 'Root nodes cannot be collapsed', ); }); // @reactVersion >= 18.0 it('should properly handle a root with no visible nodes', async () => { const Root = ({children}) => children; await act(() => render(<Root>{null}</Root>)); expect(store).toMatchInlineSnapshot(` [root] <Root> `); await act(() => render(<div />)); expect(store).toMatchInlineSnapshot(`[root]`); }); // This test is not the same cause as what's reported on GitHub, // but the resulting behavior (owner mounting after descendant) is the same. // Thec ase below is admittedly contrived and relies on side effects. // I'mnot yet sure of how to reduce the GitHub reported production case to a test though. // See https://github.com/facebook/react/issues/21445 // @reactVersion >= 18.0 it('should handle when a component mounts before its owner', async () => { const promise = new Promise(resolve => {}); let Dynamic = null; const Owner = () => { Dynamic = <Child />; readValue(promise); }; const Parent = () => { return Dynamic; }; const Child = () => null; await act(() => render( <> <React.Suspense fallback="Loading..."> <Owner /> </React.Suspense> <Parent /> </>, ), ); expect(store).toMatchInlineSnapshot(` [root] <Suspense> ▾ <Parent> <Child> [suspense-root] rects={null} <Suspense name="Unknown" rects={null}> `); }); // @reactVersion >= 18.0 it('should handle multibyte character strings', async () => { const Component = () => null; Component.displayName = '🟩💜🔵'; await act(() => render(<Component />)); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> `); }); it('should handle reorder of filtered elements', async () => { function IgnoreMePassthrough({children}) { return children; } function PassThrough({children}) { return children; } await actAsync( async () => (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]), ); await act(() => { render( <PassThrough key="e" name="e"> <IgnoreMePassthrough key="e1"> <PassThrough name="e-child-one"> <p>e1</p> </PassThrough> </IgnoreMePassthrough> <IgnoreMePassthrough key="e2"> <PassThrough name="e-child-two"> <div>e2</div> </PassThrough> </IgnoreMePassthrough> </PassThrough>, ); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <PassThrough key="e"> ▾ <PassThrough> <p> ▾ <PassThrough> <div> `); await act(() => { render( <PassThrough key="e" name="e"> <IgnoreMePassthrough key="e2"> <PassThrough name="e-child-two"> <div>e2</div> </PassThrough> </IgnoreMePassthrough> <IgnoreMePassthrough key="e1"> <PassThrough name="e-child-one"> <p>e1</p> </PassThrough> </IgnoreMePassthrough> </PassThrough>, ); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <PassThrough key="e"> ▾ <PassThrough> <div> ▾ <PassThrough> <p> `); }); describe('StrictMode compliance', () => { it('should mark strict root elements as strict', async () => { const App = () => <Component />; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, }); await act(() => { root.render(<App />); }); expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false); expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false); }); // @reactVersion >= 18.0 it('should mark non strict root elements as not strict', async () => { const App = () => <Component />; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<App />); }); expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true); expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true); }); it('should mark StrictMode subtree elements as strict', async () => { const App = () => ( <React.StrictMode> <Component /> </React.StrictMode> ); const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<App />); }); expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true); expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false); }); }); describe('collapseNodesByDefault:false', () => { beforeEach(() => { store.collapseNodesByDefault = false; }); // @reactVersion >= 18.0 it('should support mount and update operations', async () => { const Grandparent = ({count}) => ( <React.Fragment> <Parent count={count} /> <Parent count={count} /> </React.Fragment> ); const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; await act(() => render(<Grandparent count={4} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> ▾ <Parent> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> `); await act(() => render(<Grandparent count={2} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▾ <Parent> <Child key="0"> <Child key="1"> `); await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; const containerA = document.createElement('div'); const containerB = document.createElement('div'); await act(() => { legacyRender(<Parent key="A" count={3} />, containerA); legacyRender(<Parent key="B" count={2} />, containerB); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> [root] ▾ <Parent key="B"> <Child key="0"> <Child key="1"> `); await act(() => { legacyRender(<Parent key="A" count={4} />, containerA); legacyRender(<Parent key="B" count={1} />, containerB); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> [root] ▾ <Parent key="B"> <Child key="0"> `); await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> `); await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); await act(() => { rootA.render(<Parent key="A" count={3} />); rootB.render(<Parent key="B" count={2} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> [root] ▾ <Parent key="B"> <Child key="0"> <Child key="1"> `); await act(() => { rootA.render(<Parent key="A" count={4} />); rootB.render(<Parent key="B" count={1} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> [root] ▾ <Parent key="B"> <Child key="0"> `); await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▾ <Parent key="A"> <Child key="0"> <Child key="1"> <Child key="2"> <Child key="3"> `); await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => ( <div> <div> <Parent /> </div> <Parent /> </div> ); const Parent = () => ( <div> <Child /> </div> ); const Child = () => <div>Hi!</div>; await act(() => render(<Grandparent count={4} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child> ▾ <Parent> <Child> `); }); // @reactVersion >= 18.0 it('should display Suspense nodes properly in various states', async () => { const Loading = () => <div>Loading...</div>; const never = new Promise(() => {}); const SuspendingComponent = () => { readValue(never); }; const Component = () => { return <div>Hello</div>; }; const Wrapper = ({shouldSuspense}) => ( <React.Fragment> <Component key="Outside" /> <React.Suspense fallback={<Loading />}> {shouldSuspense ? ( <SuspendingComponent /> ) : ( <Component key="Inside" /> )} </React.Suspense> </React.Fragment> ); await act(() => render(<Wrapper shouldSuspense={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense> <Loading> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="Wrapper" rects={null}> `); await act(() => { render(<Wrapper shouldSuspense={false} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense> <Component key="Inside"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}> `); }); // @reactVersion >= 18.0 it('should support nested Suspense nodes', async () => { const Component = () => null; const Loading = () => <div>Loading...</div>; const never = new Promise(() => {}); const Never = () => { readValue(never); }; const Wrapper = ({ suspendFirst = false, suspendSecond = false, suspendParent = false, }) => ( <React.Fragment> <Component key="Outside" /> <React.Suspense name="parent" fallback={<Loading key="Parent Fallback" />}> <Component key="Unrelated at Start" /> <React.Suspense name="one" fallback={<Loading key="Suspense 1 Fallback" />}> {suspendFirst ? ( <Never /> ) : ( <Component key="Suspense 1 Content" /> )} </React.Suspense> <React.Suspense name="two" fallback={<Loading key="Suspense 2 Fallback" />}> {suspendSecond ? ( <Never /> ) : ( <Component key="Suspense 2 Content" /> )} </React.Suspense> <React.Suspense name="three" fallback={<Loading key="Suspense 3 Fallback" />}> <Never /> </React.Suspense> {suspendParent && <Never />} <Component key="Unrelated at End" /> </React.Suspense> </React.Fragment> ); await actAsync(() => render( <Wrapper suspendParent={false} suspendFirst={false} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={true} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={false} suspendSecond={true} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Loading key="Suspense 2 Fallback"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={true} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={true} suspendFirst={true} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Loading key="Parent Fallback"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={true} suspendSecond={true} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Loading key="Suspense 2 Fallback"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={false} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); const rendererID = getRendererID(); await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, forceFallback: true, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, forceFallback: true, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Loading key="Parent Fallback"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={true} suspendSecond={true} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Loading key="Parent Fallback"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await actAsync(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, forceFallback: false, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Loading key="Suspense 2 Fallback"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, forceFallback: false, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Loading key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Loading key="Suspense 2 Fallback"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); await act(() => render( <Wrapper suspendParent={false} suspendFirst={false} suspendSecond={false} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Loading key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}> <Suspense name="one" rects={null}> <Suspense name="two" rects={null}> <Suspense name="three" rects={null}> `); }); // @reactVersion >= 18.0 it('can override multiple Suspense simultaneously', async () => { const Component = () => { return <div>Hello</div>; }; const App = () => ( <React.Fragment> <Component key="Outside" /> <React.Suspense name="parent" fallback={<Component key="Parent Fallback" />}> <Component key="Unrelated at Start" /> <React.Suspense name="one" fallback={<Component key="Suspense 1 Fallback" />}> <Component key="Suspense 1 Content" /> </React.Suspense> <React.Suspense name="two" fallback={<Component key="Suspense 2 Fallback" />}> <Component key="Suspense 2 Content" /> </React.Suspense> <React.Suspense name="three" fallback={<Component key="Suspense 3 Fallback" />}> <Component key="Suspense 3 Content" /> </React.Suspense> <Component key="Unrelated at End" /> </React.Suspense> </React.Fragment> ); await actAsync(() => render(<App />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Component key="Suspense 3 Content"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}> <Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}> `); await actAsync(() => { agent.overrideSuspenseMilestone({ suspendedSet: [ store.getElementIDAtIndex(4), store.getElementIDAtIndex(8), ], }); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Fallback"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Component key="Suspense 3 Fallback"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}> <Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}> `); await actAsync(() => { agent.overrideSuspenseMilestone({ suspendedSet: [], }); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Component key="Outside"> ▾ <Suspense name="parent"> <Component key="Unrelated at Start"> ▾ <Suspense name="one"> <Component key="Suspense 1 Content"> ▾ <Suspense name="two"> <Component key="Suspense 2 Content"> ▾ <Suspense name="three"> <Component key="Suspense 3 Content"> <Component key="Unrelated at End"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}> <Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}> <Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}> `); }); it('should display a partially rendered SuspenseList', async () => { const Loading = () => <div>Loading...</div>; const never = new Promise(() => {}); const SuspendingComponent = () => { readValue(never); }; const Component = () => { return <div>Hello</div>; }; const Wrapper = ({shouldSuspense}) => ( <React.Fragment> <React.unstable_SuspenseList revealOrder="forwards" tail="collapsed"> <Component key="A" /> <React.Suspense fallback={<Loading />}> {shouldSuspense ? <SuspendingComponent /> : <Component key="B" />} </React.Suspense> <Component key="C" /> </React.unstable_SuspenseList> </React.Fragment> ); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<Wrapper shouldSuspense={true} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▾ <SuspenseList> <Component key="A"> ▾ <Suspense> <Loading> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="Wrapper" rects={null}> `); await act(() => { root.render(<Wrapper shouldSuspense={false} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▾ <SuspenseList> <Component key="A"> ▾ <Suspense> <Component key="B"> <Component key="C"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}> `); }); // @reactVersion >= 18.0 it('should support collapsing parts of the tree', async () => { const Grandparent = ({count}) => ( <React.Fragment> <Parent count={count} /> <Parent count={count} /> </React.Fragment> ); const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; await act(() => render(<Grandparent count={2} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▾ <Parent> <Child key="0"> <Child key="1"> `); const grandparentID = store.getElementIDAtIndex(0); const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(4); await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▾ <Parent> <Child key="0"> <Child key="1"> `); await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▸ <Parent> `); await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▸ <Parent> `); await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Grandparent> `); await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▸ <Parent> `); }); // @reactVersion >= 18.0 it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; const Foo = () => [<Component key="0" />]; const Bar = () => [<Component key="0" />, <Component key="1" />]; const foo = <Foo key="foo" />; const bar = <Bar key="bar" />; await act(() => render(<Root>{[foo, bar]}</Root>)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Root> ▾ <Foo key="foo"> <Component key="0"> ▾ <Bar key="bar"> <Component key="0"> <Component key="1"> `); await act(() => render(<Root>{[bar, foo]}</Root>)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Root> ▾ <Bar key="bar"> <Component key="0"> <Component key="1"> ▾ <Foo key="foo"> <Component key="0"> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), ); expect(store).toMatchInlineSnapshot(` [root] ▸ <Root> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Root> ▾ <Bar key="bar"> <Component key="0"> <Component key="1"> ▾ <Foo key="foo"> <Component key="0"> `); }); }); describe('collapseNodesByDefault:true', () => { beforeEach(() => { store.collapseNodesByDefault = true; }); // @reactVersion >= 18.0 it('should support mount and update operations', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; await act(() => render( <React.Fragment> <Parent count={1} /> <Parent count={3} /> </React.Fragment>, ), ); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent> ▸ <Parent> `); await act(() => render( <React.Fragment> <Parent count={2} /> <Parent count={1} /> </React.Fragment>, ), ); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent> ▸ <Parent> `); await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; const containerA = document.createElement('div'); const containerB = document.createElement('div'); await act(() => { legacyRender(<Parent key="A" count={3} />, containerA); legacyRender(<Parent key="B" count={2} />, containerB); }); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> [root] ▸ <Parent key="B"> `); await act(() => { legacyRender(<Parent key="A" count={4} />, containerA); legacyRender(<Parent key="B" count={1} />, containerB); }); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> [root] ▸ <Parent key="B"> `); await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> `); await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); await act(() => { rootA.render(<Parent key="A" count={3} />); rootB.render(<Parent key="B" count={2} />); }); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> [root] ▸ <Parent key="B"> `); await act(() => { rootA.render(<Parent key="A" count={4} />); rootB.render(<Parent key="B" count={1} />); }); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> [root] ▸ <Parent key="B"> `); await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▸ <Parent key="A"> `); await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => ( <div> <div> <Parent /> </div> <Parent /> </div> ); const Parent = () => ( <div> <Child /> </div> ); const Child = () => <div>Hi!</div>; await act(() => render(<Grandparent count={4} />)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Grandparent> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▸ <Parent> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child> ▸ <Parent> `); }); // @reactVersion >= 18.0 it('should display Suspense nodes properly in various states', async () => { const Loading = () => <div>Loading...</div>; const never = new Promise(() => {}); const SuspendingComponent = () => { readValue(never); }; const Component = () => { return <div>Hello</div>; }; const Wrapper = ({shouldSuspense}) => ( <React.Fragment> <Component key="Outside" /> <React.Suspense fallback={<Loading />}> {shouldSuspense ? ( <SuspendingComponent /> ) : ( <Component key="Inside" /> )} </React.Suspense> </React.Fragment> ); await act(() => render(<Wrapper shouldSuspense={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Wrapper> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="Wrapper" rects={null}> `); // This test isn't meaningful unless we expand the suspended tree await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), ); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense> <Loading> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:10,height:1}]} <Suspense name="Wrapper" rects={null}> `); await act(() => { render(<Wrapper shouldSuspense={false} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> <Component key="Outside"> ▾ <Suspense> <Component key="Inside"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]} <Suspense name="Wrapper" rects={[{x:1,y:2,width:5,height:1}]}> `); }); // @reactVersion >= 18.0 it('should support expanding parts of the tree', async () => { const Grandparent = ({count}) => ( <React.Fragment> <Parent count={count} /> <Parent count={count} /> </React.Fragment> ); const Parent = ({count}) => new Array(count).fill(true).map((_, index) => <Child key={index} />); const Child = () => <div>Hi!</div>; await act(() => render(<Grandparent count={2} />)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Grandparent> `); const grandparentID = store.getElementIDAtIndex(0); await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▸ <Parent> `); const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(2); await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▸ <Parent> `); await act(() => store.toggleIsCollapsed(parentTwoID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▾ <Parent> <Child key="0"> <Child key="1"> ▾ <Parent> <Child key="0"> <Child key="1"> `); await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▾ <Parent> <Child key="0"> <Child key="1"> `); await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Grandparent> ▸ <Parent> ▸ <Parent> `); await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Grandparent> `); }); // @reactVersion >= 18.0 it('should support expanding deep parts of the tree', async () => { const Wrapper = ({forwardedRef}) => ( <Nested depth={3} forwardedRef={forwardedRef} /> ); const Nested = ({depth, forwardedRef}) => depth > 0 ? ( <Nested depth={depth - 1} forwardedRef={forwardedRef} /> ) : ( <div ref={forwardedRef} /> ); const ref = React.createRef(); await act(() => render(<Wrapper forwardedRef={ref} />)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Wrapper> `); const deepestedNodeID = agent.getIDForHostInstance(ref.current); await act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▾ <Nested> ▾ <Nested> ▾ <Nested> <Nested> `); const rootID = store.getElementIDAtIndex(0); await act(() => store.toggleIsCollapsed(rootID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Wrapper> `); await act(() => store.toggleIsCollapsed(rootID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▾ <Nested> ▾ <Nested> ▾ <Nested> <Nested> `); const id = store.getElementIDAtIndex(1); await act(() => store.toggleIsCollapsed(id, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▸ <Nested> `); await act(() => store.toggleIsCollapsed(id, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ <Wrapper> ▾ <Nested> ▾ <Nested> ▾ <Nested> <Nested> `); }); // @reactVersion >= 18.0 it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; const Foo = () => [<Component key="0" />]; const Bar = () => [<Component key="0" />, <Component key="1" />]; const foo = <Foo key="foo" />; const bar = <Bar key="bar" />; await act(() => render(<Root>{[foo, bar]}</Root>)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Root> `); await act(() => render(<Root>{[bar, foo]}</Root>)); expect(store).toMatchInlineSnapshot(` [root] ▸ <Root> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Root> ▸ <Bar key="bar"> ▸ <Foo key="foo"> `); await act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Root> ▾ <Bar key="bar"> <Component key="0"> <Component key="1"> ▾ <Foo key="foo"> <Component key="0"> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), ); expect(store).toMatchInlineSnapshot(` [root] ▸ <Root> `); }); // @reactVersion >= 18.0 it('should not add new nodes when suspense is toggled', async () => { const SuspenseTree = () => { return ( <React.Suspense fallback={<Fallback>Loading outer</Fallback>}> <Parent /> </React.Suspense> ); }; const Fallback = () => null; const Parent = () => <Child />; const Child = () => null; await act(() => render(<SuspenseTree />)); expect(store).toMatchInlineSnapshot(` [root] ▸ <SuspenseTree> [suspense-root] rects={null} <Suspense name="SuspenseTree" rects={null}> `); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), ); await act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <SuspenseTree> ▾ <Suspense> ▸ <Parent> [suspense-root] rects={null} <Suspense name="SuspenseTree" rects={null}> `); const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, forceFallback: true, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <SuspenseTree> ▾ <Suspense> <Fallback> [suspense-root] rects={null} <Suspense name="SuspenseTree" rects={null}> `); await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, forceFallback: false, }), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <SuspenseTree> ▾ <Suspense> ▸ <Parent> [suspense-root] rects={null} <Suspense name="SuspenseTree" rects={null}> `); }); }); describe('getIndexOfElementID', () => { beforeEach(() => { store.collapseNodesByDefault = false; }); // @reactVersion >= 18.0 it('should support a single root with a single child', async () => { const Grandparent = () => ( <React.Fragment> <Parent /> <Parent /> </React.Fragment> ); const Parent = () => <Child />; const Child = () => null; await act(() => render(<Grandparent />)); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support multiple roots with one children each', async () => { const Grandparent = () => <Parent />; const Parent = () => <Child />; const Child = () => null; await act(() => { render(<Grandparent />); render(<Grandparent />); }); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support a single root with multiple top level children', async () => { const Grandparent = () => <Parent />; const Parent = () => <Child />; const Child = () => null; await act(() => render( <React.Fragment> <Grandparent /> <Grandparent /> </React.Fragment>, ), ); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); // @reactVersion >= 18.0 it('should support multiple roots with multiple top level children', async () => { const Grandparent = () => <Parent />; const Parent = () => <Child />; const Child = () => null; await act(() => { render( <React.Fragment> <Grandparent /> <Grandparent /> </React.Fragment>, ); createContainer(); render( <React.Fragment> <Grandparent /> <Grandparent /> </React.Fragment>, ); }); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); } }); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('detects and updates profiling support based on the attached roots (legacy render)', async () => { const Component = () => null; const containerA = document.createElement('div'); const containerB = document.createElement('div'); expect(store.rootSupportsBasicProfiling).toBe(false); await act(() => legacyRender(<Component />, containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); await act(() => legacyRender(<Component />, containerB)); await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18 it('detects and updates profiling support based on the attached roots (createRoot)', async () => { const Component = () => null; const containerA = document.createElement('div'); const containerB = document.createElement('div'); const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); expect(store.rootSupportsBasicProfiling).toBe(false); await act(() => rootA.render(<Component />)); expect(store.rootSupportsBasicProfiling).toBe(true); await act(() => rootB.render(<Component />)); await act(() => rootA.unmount()); expect(store.rootSupportsBasicProfiling).toBe(true); await act(() => rootB.unmount()); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18.0 it('should properly serialize non-string key values', async () => { const Child = () => null; // Bypass React element's automatic stringifying of keys intentionally. // This is pretty hacky. const fauxElement = Object.assign({}, <Child />, {key: 123}); await act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] <Child key="123"> `); }); it('should show the right display names for special component types', async () => { const MyComponent = (props, ref) => null; const ForwardRefComponent = React.forwardRef(MyComponent); const MyComponent2 = (props, ref) => null; const ForwardRefComponentWithAnonymousFunction = React.forwardRef(() => ( <MyComponent2 /> )); const MyComponent3 = (props, ref) => null; const ForwardRefComponentWithCustomDisplayName = React.forwardRef(MyComponent3); ForwardRefComponentWithCustomDisplayName.displayName = 'Custom'; const MyComponent4 = (props, ref) => null; const MemoComponent = React.memo(MyComponent4); const MemoForwardRefComponent = React.memo(ForwardRefComponent); const FakeHigherOrderComponent = () => null; FakeHigherOrderComponent.displayName = 'withFoo(withBar(Baz))'; const MemoizedFakeHigherOrderComponent = React.memo( FakeHigherOrderComponent, ); const ForwardRefFakeHigherOrderComponent = React.forwardRef( FakeHigherOrderComponent, ); const MemoizedFakeHigherOrderComponentWithDisplayNameOverride = React.memo( FakeHigherOrderComponent, ); MemoizedFakeHigherOrderComponentWithDisplayNameOverride.displayName = 'memoRefOverride'; const ForwardRefFakeHigherOrderComponentWithDisplayNameOverride = React.forwardRef(FakeHigherOrderComponent); ForwardRefFakeHigherOrderComponentWithDisplayNameOverride.displayName = 'forwardRefOverride'; const App = () => ( <React.Fragment> <MyComponent /> <ForwardRefComponent /> <ForwardRefComponentWithAnonymousFunction /> <ForwardRefComponentWithCustomDisplayName /> <MemoComponent /> <MemoForwardRefComponent /> <FakeHigherOrderComponent /> <MemoizedFakeHigherOrderComponent /> <ForwardRefFakeHigherOrderComponent /> <MemoizedFakeHigherOrderComponentWithDisplayNameOverride /> <ForwardRefFakeHigherOrderComponentWithDisplayNameOverride /> </React.Fragment> ); // Render once to start fetching the lazy component await act(() => render(<App />)); await Promise.resolve(); // Render again after it resolves await act(() => render(<App />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <MyComponent> <MyComponent> [ForwardRef] ▾ <Anonymous> [ForwardRef] <MyComponent2> <Custom> <MyComponent4> [Memo] ▾ <MyComponent> [Memo] <MyComponent> [ForwardRef] <Baz> [withFoo][withBar] <Baz> [Memo][withFoo][withBar] <Baz> [ForwardRef][withFoo][withBar] <memoRefOverride> <forwardRefOverride> `); }); describe('Lazy', () => { async function fakeImport(result) { return {default: result}; } const LazyInnerComponent = () => null; const App = ({renderChildren}) => { if (renderChildren) { return ( <React.Suspense fallback="Loading..."> <LazyComponent /> </React.Suspense> ); } else { return null; } }; let LazyComponent; beforeEach(() => { LazyComponent = React.lazy(() => fakeImport(LazyInnerComponent)); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support Lazy components (legacy render)', async () => { const container = document.createElement('div'); // Render once to start fetching the lazy component await act(() => legacyRender(<App renderChildren={true} />, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Suspense> `); await Promise.resolve(); // Render again after it resolves await act(() => legacyRender(<App renderChildren={true} />, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Suspense> <LazyInnerComponent> `); // Render again to unmount it await act(() => legacyRender(<App renderChildren={false} />, container)); expect(store).toMatchInlineSnapshot(` [root] <App> `); }); // @reactVersion >= 18.0 it('should support Lazy components in (createRoot)', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component await act(() => root.render(<App renderChildren={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Suspense> [suspense-root] rects={null} <Suspense name="App" rects={null}> `); await Promise.resolve(); // Render again after it resolves await act(() => root.render(<App renderChildren={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Suspense> <LazyInnerComponent> [suspense-root] rects={null} <Suspense name="App" rects={null}> `); // Render again to unmount it await act(() => root.render(<App renderChildren={false} />)); expect(store).toMatchInlineSnapshot(` [root] <App> `); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => { const container = document.createElement('div'); // Render once to start fetching the lazy component await act(() => legacyRender(<App renderChildren={true} />, container)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Suspense> `); // Render again to unmount it before it finishes loading await act(() => legacyRender(<App renderChildren={false} />, container)); expect(store).toMatchInlineSnapshot(` [root] <App> `); }); // @reactVersion >= 18.0 // @reactVersion < 19 it('should support Lazy components that are unmounted before they finish loading in (createRoot)', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component await act(() => root.render(<App renderChildren={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Suspense> [suspense-root] rects={null} <Suspense name="App" rects={null}> `); // Render again to unmount it before it finishes loading await act(() => root.render(<App renderChildren={false} />)); expect(store).toMatchInlineSnapshot(` [root] <App> `); }); }); describe('inline errors and warnings', () => { // @reactVersion >= 18.0 it('during render are counted', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render(<Example />)); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] <Example> ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render(<Example rerender={1} />)); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ `); }); // @reactVersion >= 18.0 it('during layout get counted', async () => { function Example() { React.useLayoutEffect(() => { console.error('test-only: layout error'); console.warn('test-only: layout warning'); }); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render(<Example />)); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] <Example> ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render(<Example rerender={1} />)); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ `); }); describe('during passive effects', () => { function flushPendingBridgeOperations() { jest.runOnlyPendingTimers(); } // @reactVersion >= 18.0 it('are counted (after no delay)', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); console.warn('test-only: passive warning'); }); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => { render(<Example />); }, false); }); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] <Example> ✕⚠ `); await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 it('are flushed early when there is a new commit', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); console.warn('test-only: passive warning'); }); return null; } function Noop() { return null; } withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { render( <> <Example /> </>, ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 1 [root] <Example> ✕⚠ `); // Before warnings and errors have flushed, flush another commit. act(() => { render( <> <Example /> <Noop /> </>, ); }, false); flushPendingBridgeOperations(); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ <Noop> `); }); await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088 // The warning is moved to the Child instead of the Parent. // @reactVersion >= 19.0.1 it('from react get counted [React >= 19.0.1]', async () => { function Example() { return [<Child />]; } function Child() { return null; } withErrorsOrWarningsIgnored( ['Each child in a list should have a unique "key" prop'], () => { act(() => render(<Example />)); }, ); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 0 [root] ▾ <Example> <Child> ✕ `); }); // @reactVersion >= 18.0 // @reactVersion < 19.0 it('from react get counted [React 18.x]', async () => { function Example() { return [<Child />]; } function Child() { return null; } withErrorsOrWarningsIgnored( ['Each child in a list should have a unique "key" prop'], () => { act(() => render(<Example />)); }, ); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 0 [root] ▾ <Example> ✕ <Child> `); }); // @reactVersion >= 18.0 it('can be cleared for the whole app', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <Example /> <Example /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ <Example> ✕⚠ `); const { clearErrorsAndWarnings, } = require('react-devtools-shared/src/backendAPI'); clearErrorsAndWarnings({bridge, store}); // flush events to the renderer jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` [root] <Example> <Example> `); }); // @reactVersion >= 18.0 it('can be cleared for particular Fiber (only warnings)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <Example /> <Example /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ <Example> ✕⚠ `); const id = ((store.getElementIDAtIndex(1): any): number); const rendererID = store.getRendererIDForElement(id); const { clearWarningsForElement, } = require('react-devtools-shared/src/backendAPI'); clearWarningsForElement({bridge, id, rendererID}); // Flush events to the renderer. jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 1 [root] <Example> ✕⚠ <Example> ✕ `); }); // @reactVersion >= 18.0 it('can be cleared for a particular Fiber (only errors)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <Example /> <Example /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <Example> ✕⚠ <Example> ✕⚠ `); const id = ((store.getElementIDAtIndex(1): any): number); const rendererID = store.getRendererIDForElement(id); const { clearErrorsForElement, } = require('react-devtools-shared/src/backendAPI'); clearErrorsForElement({bridge, id, rendererID}); // Flush events to the renderer. jest.runAllTimers(); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 2 [root] <Example> ✕⚠ <Example> ⚠ `); }); // @reactVersion >= 18.0 it('are updated when fibers are removed from the tree', async () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; } function ComponentWithError() { console.error('test-only: render error'); return null; } function ComponentWithWarningAndError() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <ComponentWithError /> <ComponentWithWarning /> <ComponentWithWarningAndError /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 2, ⚠ 2 [root] <ComponentWithError> ✕ <ComponentWithWarning> ⚠ <ComponentWithWarningAndError> ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <ComponentWithWarning /> <ComponentWithWarningAndError /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 1, ⚠ 2 [root] <ComponentWithWarning> ⚠ <ComponentWithWarningAndError> ✕⚠ `); withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render( <React.Fragment> <ComponentWithWarning /> </React.Fragment>, ), ); }); expect(store).toMatchInlineSnapshot(` ✕ 0, ⚠ 2 [root] <ComponentWithWarning> ⚠ `); withErrorsOrWarningsIgnored(['test-only:'], async () => { await act(() => render(<React.Fragment />)); }); expect(store).toMatchInlineSnapshot(``); expect(store.componentWithErrorCount).toBe(0); expect(store.componentWithWarningCount).toBe(0); }); // Regression test for https://github.com/facebook/react/issues/23202 // @reactVersion >= 18.0 it('suspense boundary children should not double unmount and error', async () => { async function fakeImport(result) { return {default: result}; } const ChildA = () => null; const ChildB = () => null; const LazyChildA = React.lazy(() => fakeImport(ChildA)); const LazyChildB = React.lazy(() => fakeImport(ChildB)); function App({renderA}) { return ( <React.Suspense> {renderA ? <LazyChildA /> : <LazyChildB />} </React.Suspense> ); } await actAsync(() => render(<App renderA={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Suspense> <ChildA> [suspense-root] rects={null} <Suspense name="App" rects={null}> `); await actAsync(() => render(<App renderA={false} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Suspense> <ChildB> [suspense-root] rects={null} <Suspense name="App" rects={null}> `); }); }); // @reactVersion > 18.2 it('does not show server components without any children reified children', async () => { // A Server Component that doesn't render into anything on the client doesn't show up. const ServerPromise = Promise.resolve(null); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ServerPromise; await actAsync(() => render(<App />)); expect(store).toMatchInlineSnapshot(` [root] <App> `); }); // @reactVersion > 18.2 it('does show a server component that renders into a filtered node', async () => { const ServerPromise = Promise.resolve(<div />); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ServerPromise; await actAsync(() => render(<App />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <ServerComponent> [Server] `); }); it('can render the same server component twice', async () => { function ClientComponent() { return <div />; } const ServerPromise = Promise.resolve(<ClientComponent />); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = () => ( <> {ServerPromise} <ClientComponent /> {ServerPromise} </> ); await actAsync(() => render(<App />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent> [Server] <ClientComponent> <ClientComponent> ▾ <ServerComponent> [Server] <ClientComponent> `); }); // @reactVersion > 18.2 it('collapses multiple parent server components into one', async () => { function ClientComponent() { return <div />; } const ServerPromise = Promise.resolve(<ClientComponent />); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const ServerPromise2 = Promise.resolve(<ClientComponent />); ServerPromise2._debugInfo = [ { name: 'ServerComponent2', env: 'Server', owner: null, }, ]; const App = ({initial}) => ( <> {ServerPromise} {ServerPromise} {ServerPromise2} {initial ? null : ServerPromise2} </> ); await actAsync(() => render(<App initial={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent> [Server] <ClientComponent> <ClientComponent> ▾ <ServerComponent2> [Server] <ClientComponent> `); await actAsync(() => render(<App initial={false} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent> [Server] <ClientComponent> <ClientComponent> ▾ <ServerComponent2> [Server] <ClientComponent> <ClientComponent> `); }); // @reactVersion > 18.2 it('can reparent a child when the server components change', async () => { function ClientComponent() { return <div />; } const ServerPromise = Promise.resolve(<ClientComponent />); ServerPromise._debugInfo = [ { name: 'ServerAB', env: 'Server', owner: null, }, ]; const ServerPromise2 = Promise.resolve(<ClientComponent />); ServerPromise2._debugInfo = [ { name: 'ServerA', env: 'Server', owner: null, }, { name: 'ServerB', env: 'Server', owner: null, }, ]; const App = ({initial}) => (initial ? ServerPromise : ServerPromise2); await actAsync(() => render(<App initial={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerAB> [Server] <ClientComponent> `); await actAsync(() => render(<App initial={false} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerA> [Server] ▾ <ServerB> [Server] <ClientComponent> `); }); // @reactVersion > 18.2 it('splits a server component parent when a different child appears between', async () => { function ClientComponent() { return <div />; } const ServerPromise = Promise.resolve(<ClientComponent />); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, }, ]; const App = ({initial}) => initial ? ( <> {ServerPromise} {null} {ServerPromise} </> ) : ( <> {ServerPromise} <ClientComponent /> {ServerPromise} </> ); await actAsync(() => render(<App initial={true} />)); // Initially the Server Component only appears once because the children // are consecutive. expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent> [Server] <ClientComponent> <ClientComponent> `); // Later the same instance gets split into two when it is no longer // consecutive so we need two virtual instances to represent two parents. await actAsync(() => render(<App initial={false} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent> [Server] <ClientComponent> <ClientComponent> ▾ <ServerComponent> [Server] <ClientComponent> `); }); // @reactVersion > 18.2 it('can reorder keyed server components', async () => { function ClientComponent({text}) { return <div>{text}</div>; } function getServerComponent(key) { const ServerPromise = Promise.resolve( <ClientComponent key={key} text={key} />, ); ServerPromise._debugInfo = [ { name: 'ServerComponent', env: 'Server', owner: null, key: key, }, ]; return ServerPromise; } const set1 = ['A', 'B', 'C'].map(getServerComponent); const set2 = ['B', 'A', 'D'].map(getServerComponent); const App = ({initial}) => (initial ? set1 : set2); await actAsync(() => render(<App initial={true} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent key="A"> [Server] <ClientComponent key="A"> ▾ <ServerComponent key="B"> [Server] <ClientComponent key="B"> ▾ <ServerComponent key="C"> [Server] <ClientComponent key="C"> `); await actAsync(() => render(<App initial={false} />)); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <ServerComponent key="B"> [Server] <ClientComponent key="B"> ▾ <ServerComponent key="A"> [Server] <ClientComponent key="A"> ▾ <ServerComponent key="D"> [Server] <ClientComponent key="D"> `); }); // @reactVersion >= 17.0 it('can reconcile Suspense in fallback positions', async () => { let resolveFallback; const fallbackPromise = new Promise(resolve => { resolveFallback = resolve; }); let resolveContent; const contentPromise = new Promise(resolve => { resolveContent = resolve; }); function Component({children, promise}) { if (promise) { readValue(promise); } return <div>{children}</div>; } await actAsync(() => render( <React.Suspense name="content" fallback={ <React.Suspense name="fallback" fallback={ <Component key="fallback-fallback"> Loading fallback... </Component> }> <Component key="fallback-content" promise={fallbackPromise}> Loading... </Component> </React.Suspense> }> <Component key="content" promise={contentPromise}> done </Component> </React.Suspense>, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Suspense name="content"> ▾ <Suspense name="fallback"> <Component key="fallback-fallback"> [suspense-root] rects={[{x:1,y:2,width:19,height:1}]} <Suspense name="content" rects={null}> <Suspense name="fallback" rects={null}> `); await actAsync(() => { resolveFallback(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Suspense name="content"> ▾ <Suspense name="fallback"> <Component key="fallback-content"> [suspense-root] rects={[{x:1,y:2,width:10,height:1}]} <Suspense name="content" rects={null}> <Suspense name="fallback" rects={[{x:1,y:2,width:10,height:1}]}> `); await actAsync(() => { resolveContent(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <Suspense name="content"> <Component key="content"> [suspense-root] rects={[{x:1,y:2,width:4,height:1}]} <Suspense name="content" rects={[{x:1,y:2,width:4,height:1}]}> `); }); // @reactVersion >= 17.0 it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => { let resolveHeadFallback; let resolveHeadContent; let resolveMainFallback; let resolveMainContent; function Component({children, promise}) { if (promise) { readValue(promise); } return <div>{children}</div>; } function WithSuspenseInFallback({fallbackPromise, contentPromise, name}) { return ( <React.Suspense name={name} fallback={ <React.Suspense name={`${name}-fallback`} fallback={ <Component key={`${name}-fallback-fallback`}> Loading fallback... </Component> }> <Component key={`${name}-fallback-content`} promise={fallbackPromise}> Loading... </Component> </React.Suspense> }> <Component key={`${name}-content`} promise={contentPromise}> done </Component> </React.Suspense> ); } function App({ headFallbackPromise, headContentPromise, mainContentPromise, mainFallbackPromise, tailContentPromise, tailFallbackPromise, }) { return ( <> <WithSuspenseInFallback fallbackPromise={headFallbackPromise} contentPromise={headContentPromise} name="head" /> <WithSuspenseInFallback fallbackPromise={mainFallbackPromise} contentPromise={mainContentPromise} name="main" /> </> ); } const initialHeadContentPromise = new Promise(resolve => { resolveHeadContent = resolve; }); const initialHeadFallbackPromise = new Promise(resolve => { resolveHeadFallback = resolve; }); const initialMainContentPromise = new Promise(resolve => { resolveMainContent = resolve; }); const initialMainFallbackPromise = new Promise(resolve => { resolveMainFallback = resolve; }); await actAsync(() => render( <App headFallbackPromise={initialHeadFallbackPromise} headContentPromise={initialHeadContentPromise} mainContentPromise={initialMainContentPromise} mainFallbackPromise={initialMainFallbackPromise} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <WithSuspenseInFallback> ▾ <Suspense name="head"> ▾ <Suspense name="head-fallback"> <Component key="head-fallback-fallback"> ▾ <WithSuspenseInFallback> ▾ <Suspense name="main"> ▾ <Suspense name="main-fallback"> <Component key="main-fallback-fallback"> [suspense-root] rects={[{x:1,y:2,width:19,height:1}, {x:1,y:2,width:19,height:1}]} <Suspense name="head" rects={null}> <Suspense name="head-fallback" rects={null}> <Suspense name="main" rects={null}> <Suspense name="main-fallback" rects={null}> `); await actAsync(() => { resolveHeadFallback(); resolveMainFallback(); resolveHeadContent(); resolveMainContent(); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <WithSuspenseInFallback> ▾ <Suspense name="head"> <Component key="head-content"> ▾ <WithSuspenseInFallback> ▾ <Suspense name="main"> <Component key="main-content"> [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:4,height:1}]} <Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}> <Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}> `); // Resuspend head content const nextHeadContentPromise = new Promise(resolve => { resolveHeadContent = resolve; }); await actAsync(() => render( <App headFallbackPromise={initialHeadFallbackPromise} headContentPromise={nextHeadContentPromise} mainContentPromise={initialMainContentPromise} mainFallbackPromise={initialMainFallbackPromise} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <WithSuspenseInFallback> ▾ <Suspense name="head"> ▾ <Suspense name="head-fallback"> <Component key="head-fallback-content"> ▾ <WithSuspenseInFallback> ▾ <Suspense name="main"> <Component key="main-content"> [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:4,height:1}]} <Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}> <Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}> <Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}> `); // Resuspend head fallback const nextHeadFallbackPromise = new Promise(resolve => { resolveHeadFallback = resolve; }); await actAsync(() => render( <App headFallbackPromise={nextHeadFallbackPromise} headContentPromise={nextHeadContentPromise} mainContentPromise={initialMainContentPromise} mainFallbackPromise={initialMainFallbackPromise} />, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <WithSuspenseInFallback> ▾ <Suspense name="head"> ▾ <Suspense name="head-fallback"> <Component key="head-fallback-fallback"> ▾ <WithSuspenseInFallback> ▾ <Suspense name="main"> <Component key="main-content"> [suspense-root] rects={[{x:1,y:2,width:4,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:19,height:1}, {x:1,y:2,width:4,height:1}]} <Suspense name="head" rects={[{x:1,y:2,width:4,height:1}]}> <Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}> <Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}> `); await actAsync(() => render(null)); expect(store).toMatchInlineSnapshot(``); }); it('should handle an empty root', async () => { await actAsync(() => render(null)); expect(store).toMatchInlineSnapshot(``); await actAsync(() => render(<span />)); expect(store).toMatchInlineSnapshot(`[root]`); }); // @reactVersion >= 19.0 it('should reconcile promise-as-a-child', async () => { function Component({children}) { return <div>{children}</div>; } await actAsync(() => render( <React.Suspense> {Promise.resolve(<Component key="A">A</Component>)} </React.Suspense>, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Suspense> <Component key="A"> [suspense-root] rects={[{x:1,y:2,width:1,height:1}]} <Suspense name="Unknown" rects={[{x:1,y:2,width:1,height:1}]}> `); await actAsync(() => render( <React.Suspense> {Promise.resolve(<Component key="not-A">not A</Component>)} </React.Suspense>, ), ); expect(store).toMatchInlineSnapshot(` [root] ▾ <Suspense> <Component key="not-A"> [suspense-root] rects={[{x:1,y:2,width:5,height:1}]} <Suspense name="Unknown" rects={[{x:1,y:2,width:5,height:1}]}> `); await actAsync(() => render(null)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 19 it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => { const Activity = React.Activity || React.unstable_Activity; const never = new Promise(() => {}); function Never() { readValue(never); return null; } function Component({children}) { return <div>{children}</div>; } function App({hidden}) { return ( <> <Activity mode={hidden ? 'hidden' : 'visible'}> <React.Suspense name="inside-activity"> <Component key="inside-activity">inside Activity</Component> </React.Suspense> </Activity> <React.Suspense name="outer-suspense"> <React.Suspense name="inner-suspense"> <Component key="inside-suspense">inside Suspense</Component> </React.Suspense> {hidden ? <Never /> : null} </React.Suspense> </> ); } await actAsync(() => { render(<App hidden={true} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Activity> <Suspense name="outer-suspense"> [suspense-root] rects={[{x:1,y:2,width:15,height:1}]} <Suspense name="outer-suspense" rects={null}> `); // mount as visible await actAsync(() => { render(null); }); await actAsync(() => { render(<App hidden={false} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Activity> ▾ <Suspense name="inside-activity"> <Component key="inside-activity"> ▾ <Suspense name="outer-suspense"> ▾ <Suspense name="inner-suspense"> <Component key="inside-suspense"> [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]} <Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}> <Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}> <Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}> `); await actAsync(() => { render(<App hidden={true} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> <Activity> <Suspense name="outer-suspense"> [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]} <Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}> <Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}> `); await actAsync(() => { render(<App hidden={false} />); }); expect(store).toMatchInlineSnapshot(` [root] ▾ <App> ▾ <Activity> ▾ <Suspense name="inside-activity"> <Component key="inside-activity"> ▾ <Suspense name="outer-suspense"> ▾ <Suspense name="inner-suspense"> <Component key="inside-suspense"> [suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]} <Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}> <Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}> <Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}> `); }); });