mirror of
https://github.com/facebook/react.git
synced 2026-02-26 17:55:04 +00:00
3398 lines
101 KiB
JavaScript
3398 lines
101 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}]}>
|
|
`);
|
|
});
|
|
|
|
// @reactVersion >= 19.0
|
|
it('guesses a Suspense name based on the owner', async () => {
|
|
let resolve;
|
|
const promise = new Promise(_resolve => {
|
|
resolve = _resolve;
|
|
});
|
|
function Inner() {
|
|
return (
|
|
<React.Suspense fallback={<p>Loading inner</p>}>
|
|
<p>{promise}</p>
|
|
</React.Suspense>
|
|
);
|
|
}
|
|
|
|
function Outer({children}) {
|
|
return (
|
|
<React.Suspense fallback={<p>Loading outer</p>}>
|
|
<p>{promise}</p>
|
|
{children}
|
|
</React.Suspense>
|
|
);
|
|
}
|
|
|
|
await actAsync(() => {
|
|
render(
|
|
<Outer>
|
|
<Inner />
|
|
</Outer>,
|
|
);
|
|
});
|
|
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <Outer>
|
|
<Suspense>
|
|
[suspense-root] rects={[{x:1,y:2,width:13,height:1}]}
|
|
<Suspense name="Outer" rects={null}>
|
|
`);
|
|
|
|
await actAsync(() => {
|
|
resolve('loaded');
|
|
});
|
|
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <Outer>
|
|
▾ <Suspense>
|
|
▾ <Inner>
|
|
<Suspense>
|
|
[suspense-root] rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}
|
|
<Suspense name="Outer" rects={[{x:1,y:2,width:6,height:1}, {x:1,y:2,width:6,height:1}]}>
|
|
<Suspense name="Inner" rects={[{x:1,y:2,width:6,height:1}]}>
|
|
`);
|
|
});
|
|
|
|
// @reactVersion >= 19.0
|
|
it('measures rects when reconnecting', async () => {
|
|
function Component({children, promise}) {
|
|
let content = '';
|
|
if (promise) {
|
|
const value = readValue(promise);
|
|
if (typeof value === 'string') {
|
|
content += value;
|
|
}
|
|
}
|
|
return (
|
|
<div>
|
|
{content}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function App({outer, inner}) {
|
|
return (
|
|
<React.Suspense
|
|
name="outer"
|
|
fallback={<Component key="outer-fallback">loading outer</Component>}>
|
|
<Component key="outer-content" promise={outer}>
|
|
outer content
|
|
</Component>
|
|
<React.Suspense
|
|
name="inner"
|
|
fallback={
|
|
<Component key="inner-fallback">loading inner</Component>
|
|
}>
|
|
<Component key="inner-content" promise={inner}>
|
|
inner content
|
|
</Component>
|
|
</React.Suspense>
|
|
</React.Suspense>
|
|
);
|
|
}
|
|
|
|
await actAsync(() => {
|
|
render(<App outer={null} inner={null} />);
|
|
});
|
|
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <App>
|
|
▾ <Suspense name="outer">
|
|
<Component key="outer-content">
|
|
▾ <Suspense name="inner">
|
|
<Component key="inner-content">
|
|
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
|
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
|
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
|
`);
|
|
|
|
let outerResolve;
|
|
const outerPromise = new Promise(resolve => {
|
|
outerResolve = resolve;
|
|
});
|
|
|
|
let innerResolve;
|
|
const innerPromise = new Promise(resolve => {
|
|
innerResolve = resolve;
|
|
});
|
|
await actAsync(() => {
|
|
render(<App outer={outerPromise} inner={innerPromise} />);
|
|
});
|
|
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <App>
|
|
▾ <Suspense name="outer">
|
|
<Component key="outer-fallback">
|
|
[suspense-root] rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}
|
|
<Suspense name="outer" rects={[{x:1,y:2,width:13,height:1}, {x:1,y:2,width:13,height:1}]}>
|
|
<Suspense name="inner" rects={[{x:1,y:2,width:13,height:1}]}>
|
|
`);
|
|
|
|
await actAsync(() => {
|
|
outerResolve('..');
|
|
innerResolve('.');
|
|
});
|
|
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <App>
|
|
▾ <Suspense name="outer">
|
|
<Component key="outer-content">
|
|
▾ <Suspense name="inner">
|
|
<Component key="inner-content">
|
|
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}
|
|
<Suspense name="outer" rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:14,height:1}]}>
|
|
<Suspense name="inner" rects={[{x:1,y:2,width:14,height:1}]}>
|
|
`);
|
|
});
|
|
});
|