/*** 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*/describe('Store', () => {
let React;
let ReactDOM;
let ReactDOMClient;
let agent;
let act;
let actAsync;
let bridge;
let getRendererID;
let legacyRender;
let store;
let withErrorsOrWarningsIgnored;
beforeEach(() => {
agent = global.agent;
bridge = global.bridge;
store = global.store;
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;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
});// @reactVersion >= 18.0
it('should not allow a root node to be collapsed', () => {
const Component = () => <div>Hi</div>;
act(() =>legacyRender(<Component count={4} />, document.createElement('div')),
);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', () => {
const Root = ({children}) => children;
const container = document.createElement('div');
act(() => legacyRender(<Root>{null}</Root>, container));
expect(store).toMatchInlineSnapshot(`
[root]
<Root>`);act(() => legacyRender(<div />, container));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', () => {
const promise = new Promise(resolve => {});
let Dynamic = null;
const Owner = () => {
Dynamic = <Child />;
throw promise;
};const Parent = () => {
return Dynamic;
};const Child = () => null;
const container = document.createElement('div');
act(() =>
legacyRender(
<><React.Suspense fallback="Loading...">
<Owner />
</React.Suspense>
<Parent />
</>,
container,),);expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>▾ <Parent><Child>`);});// @reactVersion >= 18.0
it('should handle multibyte character strings', () => {
const Component = () => null;
Component.displayName = '🟩💜🔵';
const container = document.createElement('div');
act(() => legacyRender(<Component />, container));
expect(store).toMatchInlineSnapshot(`
[root]<🟩💜🔵>`);
});describe('StrictMode compliance', () => {
it('should mark strict root elements as strict', () => {
const App = () => <Component />;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container, {
unstable_strictMode: true,
});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', () => {
const App = () => <Component />;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
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', () => {
const App = () => (
<React.StrictMode>
<Component />
</React.StrictMode>
);const Component = () => null;const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
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', () => {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>;const container = document.createElement('div');
act(() => legacyRender(<Grandparent count={4} />, container));
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">`);act(() => legacyRender(<Grandparent count={2} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0"><Child key="1">▾ <Parent><Child key="0"><Child key="1">`);act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots', () => {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');
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">`);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">`);act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent key="A"><Child key="0"><Child key="1"><Child key="2"><Child key="3">`);act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {const Grandparent = () => (<div><div><Parent /></div><Parent /></div>);const Parent = () => (<div><Child /></div>);const Child = () => <div>Hi!</div>;act(() =>legacyRender(<Grandparent count={4} />, document.createElement('div')),
);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child>▾ <Parent><Child>`);});// @reactVersion >= 18.0
it('should display Suspense nodes properly in various states', () => {const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {throw new Promise(() => {});};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>
);const container = document.createElement('div');
act(() => legacyRender(<Wrapper shouldSuspense={true} />, container));expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Loading>`);act(() => {legacyRender(<Wrapper shouldSuspense={false} />, container);});expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Inside">`);});// @reactVersion >= 18.0
it('should support nested Suspense nodes', () => {const Component = () => null;const Loading = () => <div>Loading...</div>;
const Never = () => {throw new Promise(() => {});};const Wrapper = ({suspendFirst = false,suspendSecond = false,suspendParent = false,}) => (<React.Fragment>
<Component key="Outside" /><React.Suspense fallback={<Loading key="Parent Fallback" />}>
<Component key="Unrelated at Start" /><React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
{suspendFirst ? (
<Never />) : (<Component key="Suspense 1 Content" />)}</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
{suspendSecond ? (
<Never />) : (<Component key="Suspense 2 Content" />)}</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
<Never /></React.Suspense>
{suspendParent && <Never />}<Component key="Unrelated at End" /></React.Suspense>
</React.Fragment>
);const container = document.createElement('div');
act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={false}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Component key="Suspense 1 Content">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={true}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={false}suspendSecond={true}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Component key="Suspense 1 Content">▾ <Suspense><Loading key="Suspense 2 Fallback">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={true}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={true}suspendFirst={true}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Loading key="Parent Fallback">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={true}suspendSecond={true}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Loading key="Suspense 2 Fallback">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={false}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Component key="Suspense 1 Content">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);const rendererID = getRendererID();act(() =>agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,forceFallback: true,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,forceFallback: true,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Loading key="Parent Fallback">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={true}suspendSecond={true}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Loading key="Parent Fallback">`);act(() =>agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,forceFallback: false,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Loading key="Suspense 2 Fallback">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,forceFallback: false,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Loading key="Suspense 1 Fallback">▾ <Suspense><Loading key="Suspense 2 Fallback">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);act(() =>legacyRender(<WrappersuspendParent={false}suspendFirst={false}suspendSecond={false}/>,container,),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Unrelated at Start">▾ <Suspense><Component key="Suspense 1 Content">▾ <Suspense><Component key="Suspense 2 Content">▾ <Suspense><Loading key="Suspense 3 Fallback"><Component key="Unrelated at End">`);});it('should display a partially rendered SuspenseList', () => {const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {throw new Promise(() => {});};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);
act(() => {root.render(<Wrapper shouldSuspense={true} />);
});expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▾ <SuspenseList><Component key="A">▾ <Suspense><Loading>`);act(() => {root.render(<Wrapper shouldSuspense={false} />);
});expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▾ <SuspenseList><Component key="A">▾ <Suspense><Component key="B"><Component key="C">`);});// @reactVersion >= 18.0
it('should support collapsing parts of the tree', () => {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>;act(() =>legacyRender(<Grandparent count={2} />, document.createElement('div')),
);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);
act(() => store.toggleIsCollapsed(parentOneID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▾ <Parent><Child key="0"><Child key="1">`);act(() => store.toggleIsCollapsed(parentTwoID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▸ <Parent>`);act(() => store.toggleIsCollapsed(parentOneID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0"><Child key="1">▸ <Parent>`);act(() => store.toggleIsCollapsed(grandparentID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>`);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', () => {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" />;const container = document.createElement('div');
act(() => legacyRender(<Root>{[foo, bar]}</Root>, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>▾ <Foo key="foo"><Component key="0">▾ <Bar key="bar"><Component key="0"><Component key="1">`);act(() => legacyRender(<Root>{[bar, foo]}</Root>, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>▾ <Bar key="bar"><Component key="0"><Component key="1">▾ <Foo key="foo"><Component key="0">`);act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>`);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', () => {const Parent = ({count}) =>new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;const container = document.createElement('div');
act(() =>legacyRender(<React.Fragment>
<Parent count={1} />
<Parent count={3} />
</React.Fragment>,
container,),);expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent>▸ <Parent>`);act(() =>legacyRender(<React.Fragment>
<Parent count={2} />
<Parent count={1} />
</React.Fragment>,
container,),);expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent>▸ <Parent>`);act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots', () => {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');
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">`);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">`);act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent key="A">`);act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {const Grandparent = () => (<div><div><Parent /></div><Parent /></div>);const Parent = () => (<div><Child /></div>);const Child = () => <div>Hi!</div>;act(() =>legacyRender(<Grandparent count={4} />, document.createElement('div')),
);expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>`);act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▸ <Parent>`);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', () => {const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {throw new Promise(() => {});};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>
);const container = document.createElement('div');
act(() => legacyRender(<Wrapper shouldSuspense={true} />, container));expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>`);// This test isn't meaningful unless we expand the suspended treeact(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Loading>`);act(() => {legacyRender(<Wrapper shouldSuspense={false} />, container);});expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper><Component key="Outside">▾ <Suspense><Component key="Inside">`);});// @reactVersion >= 18.0
it('should support expanding parts of the tree', () => {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>;act(() =>legacyRender(<Grandparent count={2} />, document.createElement('div')),
);expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>`);const grandparentID = store.getElementIDAtIndex(0);
act(() => store.toggleIsCollapsed(grandparentID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▸ <Parent>`);const parentOneID = store.getElementIDAtIndex(1);
const parentTwoID = store.getElementIDAtIndex(2);
act(() => store.toggleIsCollapsed(parentOneID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0"><Child key="1">▸ <Parent>`);act(() => store.toggleIsCollapsed(parentTwoID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0"><Child key="1">▾ <Parent><Child key="0"><Child key="1">`);act(() => store.toggleIsCollapsed(parentOneID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▾ <Parent><Child key="0"><Child key="1">`);act(() => store.toggleIsCollapsed(parentTwoID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▸ <Parent>▸ <Parent>`);act(() => store.toggleIsCollapsed(grandparentID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>`);});// @reactVersion >= 18.0
it('should support expanding deep parts of the tree', () => {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();
act(() =>legacyRender(<Wrapper forwardedRef={ref} />,document.createElement('div'),
),);expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>`);const deepestedNodeID = agent.getIDForNode(ref.current);
act(() => store.toggleIsCollapsed(deepestedNodeID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▾ <Nested>▾ <Nested>▾ <Nested><Nested>`);const rootID = store.getElementIDAtIndex(0);
act(() => store.toggleIsCollapsed(rootID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>`);act(() => store.toggleIsCollapsed(rootID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▾ <Nested>▾ <Nested>▾ <Nested><Nested>`);const id = store.getElementIDAtIndex(1);
act(() => store.toggleIsCollapsed(id, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▸ <Nested>`);act(() => store.toggleIsCollapsed(id, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>▾ <Nested>▾ <Nested>▾ <Nested><Nested>`);});// @reactVersion >= 18.0
it('should support reordering of children', () => {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" />;const container = document.createElement('div');
act(() => legacyRender(<Root>{[foo, bar]}</Root>, container));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>`);act(() => legacyRender(<Root>{[bar, foo]}</Root>, container));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>`);act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>▸ <Bar key="bar">▸ <Foo key="foo">`);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">`);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', () => {const SuspenseTree = () => {return (<React.Suspense fallback={<Fallback>Loading outer</Fallback>}>
<Parent /></React.Suspense>
);};const Fallback = () => null;const Parent = () => <Child />;const Child = () => null;act(() => legacyRender(<SuspenseTree />, document.createElement('div')));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <SuspenseTree>`);act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>▾ <Suspense>▸ <Parent>`);const rendererID = getRendererID();const suspenseID = store.getElementIDAtIndex(1);
act(() =>agent.overrideSuspense({
id: suspenseID,rendererID,forceFallback: true,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>▾ <Suspense><Fallback>`);act(() =>agent.overrideSuspense({
id: suspenseID,rendererID,forceFallback: false,}),);expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>▾ <Suspense>▸ <Parent>`);});});describe('getIndexOfElementID', () => {beforeEach(() => {store.collapseNodesByDefault = false;
});// @reactVersion >= 18.0
it('should support a single root with a single child', () => {const Grandparent = () => (<React.Fragment>
<Parent /><Parent /></React.Fragment>
);const Parent = () => <Child />;const Child = () => null;act(() => legacyRender(<Grandparent />, document.createElement('div')));
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', () => {const Grandparent = () => <Parent />;const Parent = () => <Child />;const Child = () => null;act(() => {legacyRender(<Grandparent />, document.createElement('div'));
legacyRender(<Grandparent />, document.createElement('div'));
});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', () => {const Grandparent = () => <Parent />;const Parent = () => <Child />;const Child = () => null;act(() =>legacyRender(<React.Fragment>
<Grandparent /><Grandparent /></React.Fragment>,
document.createElement('div'),
),);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', () => {const Grandparent = () => <Parent />;const Parent = () => <Child />;const Child = () => null;act(() => {legacyRender(<React.Fragment>
<Grandparent /><Grandparent /></React.Fragment>,
document.createElement('div'),
);legacyRender(<React.Fragment>
<Grandparent /><Grandparent /></React.Fragment>,
document.createElement('div'),
);});for (let i = 0; i < store.numElements; i++) {
expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
}});});// @reactVersion >= 18.0
it('detects and updates profiling support based on the attached roots', () => {
const Component = () => null;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
expect(store.rootSupportsBasicProfiling).toBe(false);
act(() => legacyRender(<Component />, containerA));
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => legacyRender(<Component />, containerB));
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store.rootSupportsBasicProfiling).toBe(false);
});// @reactVersion >= 18.0
it('should properly serialize non-string key values', () => {
const Child = () => null;
// Bypass React element's automatic stringifying of keys intentionally.
// This is pretty hacky.
const fauxElement = Object.assign({}, <Child />, {key: 123});
act(() => legacyRender([fauxElement], document.createElement('div')));
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 />
<React.unstable_Cache />
<MemoizedFakeHigherOrderComponentWithDisplayNameOverride />
<ForwardRefFakeHigherOrderComponentWithDisplayNameOverride />
</React.Fragment>
);const container = document.createElement('div');
// Render once to start fetching the lazy component
act(() => legacyRender(<App />, container));
await Promise.resolve();
// Render again after it resolves
act(() => legacyRender(<App />, container));
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]<Cache><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
it('should support Lazy components (legacy render)', async () => {
const container = document.createElement('div');
// Render once to start fetching the lazy component
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App><Suspense>`);
await Promise.resolve();
// Render again after it resolves
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App>▾ <Suspense><LazyInnerComponent>`);
// Render again to unmount it
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
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App><Suspense>`);
await Promise.resolve();
// Render again after it resolves
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App>▾ <Suspense><LazyInnerComponent>`);
// Render again to unmount it
act(() => root.render(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]<App>`);
});// @reactVersion >= 18.0
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
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App><Suspense>`);
// Render again to unmount it before it finishes loading
act(() => legacyRender(<App renderChildren={false} />, container));
expect(store).toMatchInlineSnapshot(`
[root]<App>`);
});// @reactVersion >= 18.0
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
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]▾ <App><Suspense>`);
// Render again to unmount it before it finishes loading
act(() => root.render(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]<App>`);
});});describe('inline errors and warnings', () => {
// @reactVersion >= 18.0
it('during render are counted', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => legacyRender(<Example />, container));
});expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]<Example> ✕⚠`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => legacyRender(<Example rerender={1} />, container));
});expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]<Example> ✕⚠`);
});// @reactVersion >= 18.0
it('during layout get counted', () => {
function Example() {
React.useLayoutEffect(() => {
console.error('test-only: layout error');
console.warn('test-only: layout warning');
});return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => legacyRender(<Example />, container));
});expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]<Example> ✕⚠`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => legacyRender(<Example rerender={1} />, container));
});expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]<Example> ✕⚠`);
});describe('during passive effects', () => {
function flushPendingBridgeOperations() {
jest.runOnlyPendingTimers();
}// Gross abstraction around pending passive warning/error delay.
function flushPendingPassiveErrorAndWarningCounts() {
jest.advanceTimersByTime(1000);
}// @reactVersion >= 18.0
it('are counted (after a delay)', () => {
function Example() {
React.useEffect(() => {
console.error('test-only: passive error');
console.warn('test-only: passive warning');
});return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => {
legacyRender(<Example />, container);
}, false);
});flushPendingBridgeOperations();
expect(store).toMatchInlineSnapshot(`
[root]<Example>`);
// After a delay, passive effects should be committed as well
act(flushPendingPassiveErrorAndWarningCounts, false);
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]<Example> ✕⚠`);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});// @reactVersion >= 18.0
it('are flushed early when there is a new commit', () => {
function Example() {
React.useEffect(() => {
console.error('test-only: passive error');
console.warn('test-only: passive warning');
});return null;
}function Noop() {
return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => {
legacyRender(
<><Example />
</>,
container,);}, false);flushPendingBridgeOperations();expect(store).toMatchInlineSnapshot(`
[root]
<Example>`);// Before warnings and errors have flushed, flush another commit.
act(() => {
legacyRender(
<><Example />
<Noop />
</>,
container,);}, false);flushPendingBridgeOperations();expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Example> ✕⚠<Noop>`);});// After a delay, passive effects should be committed as well
act(flushPendingPassiveErrorAndWarningCounts, false);
expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]<Example> ✕⚠<Noop>`);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(store).toMatchInlineSnapshot(``);
});});// @reactVersion >= 18.0
it('from react get counted', () => {
const container = document.createElement('div');
function Example() {
return [<Child />];
}function Child() {
return null;
}withErrorsOrWarningsIgnored(
['Warning: Each child in a list should have a unique "key" prop'],
() => {
act(() => legacyRender(<Example />, container));
},);expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]▾ <Example> ✕<Child>`);
});// @reactVersion >= 18.0
it('can be cleared for the whole app', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
legacyRender(
<React.Fragment>
<Example />
<Example />
</React.Fragment>,
container,),);});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)', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
legacyRender(
<React.Fragment>
<Example />
<Example />
</React.Fragment>,
container,),);});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)', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
legacyRender(
<React.Fragment>
<Example />
<Example />
</React.Fragment>,
container,),);});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', () => {
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;
}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
legacyRender(
<React.Fragment>
<ComponentWithError />
<ComponentWithWarning />
<ComponentWithWarningAndError />
</React.Fragment>,
container,),);});expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]
<ComponentWithError> ✕<ComponentWithWarning> ⚠<ComponentWithWarningAndError> ✕⚠`);withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>legacyRender(<React.Fragment>
<ComponentWithWarning /><ComponentWithWarningAndError /></React.Fragment>,
container,),);});expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
<ComponentWithWarning> ⚠<ComponentWithWarningAndError> ✕⚠`);withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>legacyRender(<React.Fragment>
<ComponentWithWarning /></React.Fragment>,
container,),);});expect(store).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
<ComponentWithWarning> ⚠`);withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => legacyRender(<React.Fragment />, container));
});expect(store).toMatchInlineSnapshot(`[root]`);
expect(store.errorCount).toBe(0);
expect(store.warningCount).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>
);}const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await actAsync(() => root.render(<App renderA={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>▾ <Suspense><ChildA>`);await actAsync(() => root.render(<App renderA={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>▾ <Suspense><ChildB>`);});});});