Files
react/packages/react-devtools-shared/src/__tests__/store-test.js
T
Ruslan Lesiutin a15bbe1475 refactor: data source for errors and warnings tracking is now in Store (#31010)
Stacked on https://github.com/facebook/react/pull/31009.

1. Instead of keeping `showInlineWarningsAndErrors` in `Settings`
context (which was removed in
https://github.com/facebook/react/pull/30610), `Store` will now have a
boolean flag, which controls if the UI should be displaying information
about errors and warnings.
2. The errors and warnings counters in the Tree view are now counting
only unique errors. This makes more sense, because it is part of the
Elements Tree view, so ideally it should be showing number of components
with errors and number of components of warnings. Consider this example:
2.1. Warning for element `A` was emitted once and warning for element
`B` was emitted twice.
2.2. With previous implementation, we would show `3 ⚠️`, because in
total there were 3 warnings in total. If user tries to iterate through
these, it will only take 2 steps to do the full cycle, because there are
only 2 elements with warnings (with one having same warning, which was
emitted twice).
2.3 With current implementation, we would show `2 ⚠️`. Inspecting the
element with doubled warning will still show the warning counter (2)
before the warning message.

With these changes, the feature correctly works.
https://fburl.com/a7fw92m4
2024-09-24 19:51:21 +01:00

2470 lines
68 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 getRendererID;
let legacyRender;
let store;
let withErrorsOrWarningsIgnored;
beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true;
agent = global.agent;
bridge = global.bridge;
store = global.store;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
const utils = require('./utils');
act = utils.act;
actAsync = utils.actAsync;
getRendererID = utils.getRendererID;
legacyRender = utils.legacyRender;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
});
const {render, unmount, createContainer} = getVersionedRenderImplementation();
// @reactVersion >= 18.0
it('should not allow a root node to be collapsed', () => {
const Component = () => <div>Hi</div>;
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', () => {
const Root = ({children}) => children;
act(() => render(<Root>{null}</Root>));
expect(store).toMatchInlineSnapshot(`
[root]
<Root>
`);
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', () => {
const promise = new Promise(resolve => {});
let Dynamic = null;
const Owner = () => {
Dynamic = <Child />;
throw promise;
};
const Parent = () => {
return Dynamic;
};
const Child = () => null;
act(() =>
render(
<>
<React.Suspense fallback="Loading...">
<Owner />
</React.Suspense>
<Parent />
</>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense>
▾ <Parent>
<Child>
`);
});
// @reactVersion >= 18.0
it('should handle multibyte character strings', () => {
const Component = () => null;
Component.displayName = '🟩💜🔵';
act(() => render(<Component />));
expect(store).toMatchInlineSnapshot(`
[root]
<🟩💜🔵>
`);
});
describe('StrictMode compliance', () => {
it('should mark strict root elements as strict', () => {
const App = () => <Component />;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container, {
unstable_strictMode: true,
});
act(() => {
root.render(<App />);
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(false);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
});
// @reactVersion >= 18.0
it('should mark non strict root elements as not strict', () => {
const App = () => <Component />;
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<App />);
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true);
});
it('should mark StrictMode subtree elements as strict', () => {
const App = () => (
<React.StrictMode>
<Component />
</React.StrictMode>
);
const Component = () => null;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<App />);
});
expect(store.getElementAtIndex(0).isStrictModeNonCompliant).toBe(true);
expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(false);
});
});
describe('collapseNodesByDefault:false', () => {
beforeEach(() => {
store.collapseNodesByDefault = false;
});
// @reactVersion >= 18.0
it('should support mount and update operations', () => {
const Grandparent = ({count}) => (
<React.Fragment>
<Parent count={count} />
<Parent count={count} />
</React.Fragment>
);
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
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">
`);
act(() => render(<Grandparent count={2} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
<Child key="1">
▾ <Parent>
<Child key="0">
<Child key="1">
`);
act(() => unmount());
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
act(() => {
legacyRender(<Parent key="A" count={3} />, containerA);
legacyRender(<Parent key="B" count={2} />, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent key="A">
<Child key="0">
<Child key="1">
<Child key="2">
[root]
▾ <Parent key="B">
<Child key="0">
<Child key="1">
`);
act(() => {
legacyRender(<Parent key="A" count={4} />, containerA);
legacyRender(<Parent key="B" count={1} />, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent key="A">
<Child key="0">
<Child key="1">
<Child key="2">
<Child key="3">
[root]
▾ <Parent key="B">
<Child key="0">
`);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent key="A">
<Child key="0">
<Child key="1">
<Child key="2">
<Child key="3">
`);
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots (createRoot)', () => {
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);
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">
`);
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">
`);
act(() => rootB.unmount());
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent key="A">
<Child key="0">
<Child key="1">
<Child key="2">
<Child key="3">
`);
act(() => rootA.unmount());
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {
const Grandparent = () => (
<div>
<div>
<Parent />
</div>
<Parent />
</div>
);
const Parent = () => (
<div>
<Child />
</div>
);
const Child = () => <div>Hi!</div>;
act(() => 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', () => {
const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return <div>Hello</div>;
};
const Wrapper = ({shouldSuspense}) => (
<React.Fragment>
<Component key="Outside" />
<React.Suspense fallback={<Loading />}>
{shouldSuspense ? (
<SuspendingComponent />
) : (
<Component key="Inside" />
)}
</React.Suspense>
</React.Fragment>
);
act(() => render(<Wrapper shouldSuspense={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading>
`);
act(() => {
render(<Wrapper shouldSuspense={false} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
`);
});
// @reactVersion >= 18.0
it('should support nested Suspense nodes', () => {
const Component = () => null;
const Loading = () => <div>Loading...</div>;
const Never = () => {
throw new Promise(() => {});
};
const Wrapper = ({
suspendFirst = false,
suspendSecond = false,
suspendParent = false,
}) => (
<React.Fragment>
<Component key="Outside" />
<React.Suspense fallback={<Loading key="Parent Fallback" />}>
<Component key="Unrelated at Start" />
<React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
{suspendFirst ? (
<Never />
) : (
<Component key="Suspense 1 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
{suspendSecond ? (
<Never />
) : (
<Component key="Suspense 2 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
<Never />
</React.Suspense>
{suspendParent && <Never />}
<Component key="Unrelated at End" />
</React.Suspense>
</React.Fragment>
);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={false}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Component key="Suspense 1 Content">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={true}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={false}
suspendSecond={true}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Component key="Suspense 1 Content">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={true}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={true}
suspendFirst={true}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading key="Parent Fallback">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={true}
suspendSecond={true}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={false}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Component key="Suspense 1 Content">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
const rendererID = getRendererID();
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading key="Parent Fallback">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={true}
suspendSecond={true}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading key="Parent Fallback">
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(2),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
agent.overrideSuspense({
id: store.getElementIDAtIndex(4),
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
act(() =>
render(
<Wrapper
suspendParent={false}
suspendFirst={false}
suspendSecond={false}
/>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Unrelated at Start">
▾ <Suspense>
<Component key="Suspense 1 Content">
▾ <Suspense>
<Component key="Suspense 2 Content">
▾ <Suspense>
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
`);
});
it('should display a partially rendered SuspenseList', () => {
const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return <div>Hello</div>;
};
const Wrapper = ({shouldSuspense}) => (
<React.Fragment>
<React.unstable_SuspenseList revealOrder="forwards" tail="collapsed">
<Component key="A" />
<React.Suspense fallback={<Loading />}>
{shouldSuspense ? <SuspendingComponent /> : <Component key="B" />}
</React.Suspense>
<Component key="C" />
</React.unstable_SuspenseList>
</React.Fragment>
);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<Wrapper shouldSuspense={true} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▾ <SuspenseList>
<Component key="A">
▾ <Suspense>
<Loading>
`);
act(() => {
root.render(<Wrapper shouldSuspense={false} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▾ <SuspenseList>
<Component key="A">
▾ <Suspense>
<Component key="B">
<Component key="C">
`);
});
// @reactVersion >= 18.0
it('should support collapsing parts of the tree', () => {
const Grandparent = ({count}) => (
<React.Fragment>
<Parent count={count} />
<Parent count={count} />
</React.Fragment>
);
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
act(() => 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);
act(() => store.toggleIsCollapsed(parentOneID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▾ <Parent>
<Child key="0">
<Child key="1">
`);
act(() => store.toggleIsCollapsed(parentTwoID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▸ <Parent>
`);
act(() => store.toggleIsCollapsed(parentOneID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
<Child key="1">
▸ <Parent>
`);
act(() => store.toggleIsCollapsed(grandparentID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>
`);
act(() => store.toggleIsCollapsed(grandparentID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
<Child key="1">
▸ <Parent>
`);
});
// @reactVersion >= 18.0
it('should support reordering of children', () => {
const Root = ({children}) => children;
const Component = () => null;
const Foo = () => [<Component key="0" />];
const Bar = () => [<Component key="0" />, <Component key="1" />];
const foo = <Foo key="foo" />;
const bar = <Bar key="bar" />;
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">
`);
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">
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>
▾ <Bar key="bar">
<Component key="0">
<Component key="1">
▾ <Foo key="foo">
<Component key="0">
`);
});
});
describe('collapseNodesByDefault:true', () => {
beforeEach(() => {
store.collapseNodesByDefault = true;
});
// @reactVersion >= 18.0
it('should support mount and update operations', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
act(() =>
render(
<React.Fragment>
<Parent count={1} />
<Parent count={3} />
</React.Fragment>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent>
▸ <Parent>
`);
act(() =>
render(
<React.Fragment>
<Parent count={2} />
<Parent count={1} />
</React.Fragment>,
),
);
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent>
▸ <Parent>
`);
act(() => unmount());
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
act(() => {
legacyRender(<Parent key="A" count={3} />, containerA);
legacyRender(<Parent key="B" count={2} />, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent key="A">
[root]
▸ <Parent key="B">
`);
act(() => {
legacyRender(<Parent key="A" count={4} />, containerA);
legacyRender(<Parent key="B" count={1} />, containerB);
});
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent key="A">
[root]
▸ <Parent key="B">
`);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent key="A">
`);
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should support mount and update operations for multiple roots (createRoot)', () => {
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);
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">
`);
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">
`);
act(() => rootB.unmount());
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Parent key="A">
`);
act(() => rootA.unmount());
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('should filter DOM nodes from the store tree', () => {
const Grandparent = () => (
<div>
<div>
<Parent />
</div>
<Parent />
</div>
);
const Parent = () => (
<div>
<Child />
</div>
);
const Child = () => <div>Hi!</div>;
act(() => render(<Grandparent count={4} />));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▸ <Parent>
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child>
▸ <Parent>
`);
});
// @reactVersion >= 18.0
it('should display Suspense nodes properly in various states', () => {
const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return <div>Hello</div>;
};
const Wrapper = ({shouldSuspense}) => (
<React.Fragment>
<Component key="Outside" />
<React.Suspense fallback={<Loading />}>
{shouldSuspense ? (
<SuspendingComponent />
) : (
<Component key="Inside" />
)}
</React.Suspense>
</React.Fragment>
);
act(() => render(<Wrapper shouldSuspense={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>
`);
// This test isn't meaningful unless we expand the suspended tree
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Loading>
`);
act(() => {
render(<Wrapper shouldSuspense={false} />);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
`);
});
// @reactVersion >= 18.0
it('should support expanding parts of the tree', () => {
const Grandparent = ({count}) => (
<React.Fragment>
<Parent count={count} />
<Parent count={count} />
</React.Fragment>
);
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => <div>Hi!</div>;
act(() => render(<Grandparent count={2} />));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>
`);
const grandparentID = store.getElementIDAtIndex(0);
act(() => store.toggleIsCollapsed(grandparentID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▸ <Parent>
`);
const parentOneID = store.getElementIDAtIndex(1);
const parentTwoID = store.getElementIDAtIndex(2);
act(() => store.toggleIsCollapsed(parentOneID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
<Child key="1">
▸ <Parent>
`);
act(() => store.toggleIsCollapsed(parentTwoID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▾ <Parent>
<Child key="0">
<Child key="1">
▾ <Parent>
<Child key="0">
<Child key="1">
`);
act(() => store.toggleIsCollapsed(parentOneID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▾ <Parent>
<Child key="0">
<Child key="1">
`);
act(() => store.toggleIsCollapsed(parentTwoID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>
▸ <Parent>
▸ <Parent>
`);
act(() => store.toggleIsCollapsed(grandparentID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Grandparent>
`);
});
// @reactVersion >= 18.0
it('should support expanding deep parts of the tree', () => {
const Wrapper = ({forwardedRef}) => (
<Nested depth={3} forwardedRef={forwardedRef} />
);
const Nested = ({depth, forwardedRef}) =>
depth > 0 ? (
<Nested depth={depth - 1} forwardedRef={forwardedRef} />
) : (
<div ref={forwardedRef} />
);
const ref = React.createRef();
act(() => render(<Wrapper forwardedRef={ref} />));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>
`);
const deepestedNodeID = agent.getIDForHostInstance(ref.current);
act(() => store.toggleIsCollapsed(deepestedNodeID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▾ <Nested>
▾ <Nested>
▾ <Nested>
<Nested>
`);
const rootID = store.getElementIDAtIndex(0);
act(() => store.toggleIsCollapsed(rootID, true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>
`);
act(() => store.toggleIsCollapsed(rootID, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▾ <Nested>
▾ <Nested>
▾ <Nested>
<Nested>
`);
const id = store.getElementIDAtIndex(1);
act(() => store.toggleIsCollapsed(id, true));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▸ <Nested>
`);
act(() => store.toggleIsCollapsed(id, false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Wrapper>
▾ <Nested>
▾ <Nested>
▾ <Nested>
<Nested>
`);
});
// @reactVersion >= 18.0
it('should support reordering of children', () => {
const Root = ({children}) => children;
const Component = () => null;
const Foo = () => [<Component key="0" />];
const Bar = () => [<Component key="0" />, <Component key="1" />];
const foo = <Foo key="foo" />;
const bar = <Bar key="bar" />;
act(() => render(<Root>{[foo, bar]}</Root>));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>
`);
act(() => render(<Root>{[bar, foo]}</Root>));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>
▸ <Bar key="bar">
▸ <Foo key="foo">
`);
act(() => {
store.toggleIsCollapsed(store.getElementIDAtIndex(2), false);
store.toggleIsCollapsed(store.getElementIDAtIndex(1), false);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Root>
▾ <Bar key="bar">
<Component key="0">
<Component key="1">
▾ <Foo key="foo">
<Component key="0">
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Root>
`);
});
// @reactVersion >= 18.0
it('should not add new nodes when suspense is toggled', () => {
const SuspenseTree = () => {
return (
<React.Suspense fallback={<Fallback>Loading outer</Fallback>}>
<Parent />
</React.Suspense>
);
};
const Fallback = () => null;
const Parent = () => <Child />;
const Child = () => null;
act(() => render(<SuspenseTree />));
expect(store).toMatchInlineSnapshot(`
[root]
▸ <SuspenseTree>
`);
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
`);
const rendererID = getRendererID();
const suspenseID = store.getElementIDAtIndex(1);
act(() =>
agent.overrideSuspense({
id: suspenseID,
rendererID,
forceFallback: true,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>
▾ <Suspense>
<Fallback>
`);
act(() =>
agent.overrideSuspense({
id: suspenseID,
rendererID,
forceFallback: false,
}),
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
`);
});
});
describe('getIndexOfElementID', () => {
beforeEach(() => {
store.collapseNodesByDefault = false;
});
// @reactVersion >= 18.0
it('should support a single root with a single child', () => {
const Grandparent = () => (
<React.Fragment>
<Parent />
<Parent />
</React.Fragment>
);
const Parent = () => <Child />;
const Child = () => null;
act(() => 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', () => {
const Grandparent = () => <Parent />;
const Parent = () => <Child />;
const Child = () => null;
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', () => {
const Grandparent = () => <Parent />;
const Parent = () => <Child />;
const Child = () => null;
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', () => {
const Grandparent = () => <Parent />;
const Parent = () => <Child />;
const Child = () => null;
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)', () => {
const Component = () => null;
const containerA = document.createElement('div');
const containerB = document.createElement('div');
expect(store.rootSupportsBasicProfiling).toBe(false);
act(() => legacyRender(<Component />, containerA));
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => legacyRender(<Component />, containerB));
act(() => ReactDOM.unmountComponentAtNode(containerA));
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => ReactDOM.unmountComponentAtNode(containerB));
expect(store.rootSupportsBasicProfiling).toBe(false);
});
// @reactVersion >= 18
it('detects and updates profiling support based on the attached roots (createRoot)', () => {
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);
act(() => rootA.render(<Component />));
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => rootB.render(<Component />));
act(() => rootA.unmount());
expect(store.rootSupportsBasicProfiling).toBe(true);
act(() => rootB.unmount());
expect(store.rootSupportsBasicProfiling).toBe(false);
});
// @reactVersion >= 18.0
it('should properly serialize non-string key values', () => {
const Child = () => null;
// Bypass React element's automatic stringifying of keys intentionally.
// This is pretty hacky.
const fauxElement = Object.assign({}, <Child />, {key: 123});
act(() => 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
act(() => render(<App />));
await Promise.resolve();
// Render again after it resolves
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
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
`);
await Promise.resolve();
// Render again after it resolves
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
`);
// Render again to unmount it
act(() => legacyRender(<App renderChildren={false} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
`);
});
// @reactVersion >= 18.0
it('should support Lazy components in (createRoot)', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
// Render once to start fetching the lazy component
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
`);
await Promise.resolve();
// Render again after it resolves
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
`);
// Render again to unmount it
act(() => root.render(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
`);
});
// @reactVersion >= 18.0
// @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
act(() => legacyRender(<App renderChildren={true} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
`);
// Render again to unmount it before it finishes loading
act(() => legacyRender(<App renderChildren={false} />, container));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
`);
});
// @reactVersion >= 18.0
// @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
act(() => root.render(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
<Suspense>
`);
// Render again to unmount it before it finishes loading
act(() => root.render(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
<App>
`);
});
});
describe('inline errors and warnings', () => {
// @reactVersion >= 18.0
it('during render are counted', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => render(<Example />));
});
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1
[root]
<Example> ✕⚠
`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => render(<Example rerender={1} />));
});
expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2
[root]
<Example> ✕⚠
`);
});
// @reactVersion >= 18.0
it('during layout get counted', () => {
function Example() {
React.useLayoutEffect(() => {
console.error('test-only: layout error');
console.warn('test-only: layout warning');
});
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => render(<Example />));
});
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1
[root]
<Example> ✕⚠
`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
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)', () => {
function Example() {
React.useEffect(() => {
console.error('test-only: passive error');
console.warn('test-only: passive warning');
});
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => {
render(<Example />);
}, false);
});
flushPendingBridgeOperations();
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 1
[root]
<Example> ✕⚠
`);
act(() => unmount());
expect(store).toMatchInlineSnapshot(``);
});
// @reactVersion >= 18.0
it('are flushed early when there is a new commit', () => {
function Example() {
React.useEffect(() => {
console.error('test-only: passive error');
console.warn('test-only: passive warning');
});
return null;
}
function Noop() {
return null;
}
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>
`);
});
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]', () => {
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]', () => {
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', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
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)', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
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)', () => {
function Example() {
console.error('test-only: render error');
console.warn('test-only: render warning');
return null;
}
withErrorsOrWarningsIgnored(['test-only:'], () => {
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', () => {
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:'], () => {
act(() =>
render(
<React.Fragment>
<ComponentWithError />
<ComponentWithWarning />
<ComponentWithWarningAndError />
</React.Fragment>,
),
);
});
expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 2
[root]
<ComponentWithError> ✕
<ComponentWithWarning> ⚠
<ComponentWithWarningAndError> ✕⚠
`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
render(
<React.Fragment>
<ComponentWithWarning />
<ComponentWithWarningAndError />
</React.Fragment>,
),
);
});
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 2
[root]
<ComponentWithWarning> ⚠
<ComponentWithWarningAndError> ✕⚠
`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() =>
render(
<React.Fragment>
<ComponentWithWarning />
</React.Fragment>,
),
);
});
expect(store).toMatchInlineSnapshot(`
✕ 0, ⚠ 2
[root]
<ComponentWithWarning> ⚠
`);
withErrorsOrWarningsIgnored(['test-only:'], () => {
act(() => render(<React.Fragment />));
});
expect(store).toMatchInlineSnapshot(`[root]`);
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>
`);
await actAsync(() => render(<App renderA={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<ChildB>
`);
});
});
// @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">
`);
});
});