Files
react/packages/react-devtools-shared/src/__tests__/store-test.js
Sebastian Markbåge 7385d1f61a [DevTools] Add inspection button to Suspense tab (#34867)
Add inspection button to Suspense tab which lets you select only among
Suspense nodes. It highlights all the DOM nodes in the root of the
Suspense node instead of just the DOM element you hover. The name is
inferred.

<img width="1172" height="841" alt="Screenshot 2025-10-15 at 8 03 34 PM"
src="https://github.com/user-attachments/assets/f04d965b-ef6e-4196-9ba0-51626148fa1a"
/>
2025-10-16 10:49:23 -04:00

3247 lines
97 KiB
JavaScript

/**
* 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).id;
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}]}>
`);
});
});