/*** 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 typeof ReactTestRenderer from 'react-test-renderer';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type Store from 'react-devtools-shared/src/devtools/store';
import type {
DispatcherContext,
StateContext,
} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
describe('TreeListContext', () => {
let React;
let ReactDOM;
let TestRenderer: ReactTestRenderer;
let bridge: FrontendBridge;
let legacyRender;
let store: Store;
let utils;
let withErrorsOrWarningsIgnored;
let BridgeContext;
let StoreContext;
let TreeContext;
let dispatch: DispatcherContext;
let state: StateContext;
beforeEach(() => {
utils = require('./utils');
utils.beforeEachProfiling();
legacyRender = utils.legacyRender;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
bridge = global.bridge;
store = global.store;
store.collapseNodesByDefault = false;
React = require('react');
ReactDOM = require('react-dom');
TestRenderer = utils.requireTestRenderer();
BridgeContext =
require('react-devtools-shared/src/devtools/views/context').BridgeContext;
StoreContext =
require('react-devtools-shared/src/devtools/views/context').StoreContext;
TreeContext = require('react-devtools-shared/src/devtools/views/Components/TreeContext');
});afterEach(() => {
// Reset between tests
dispatch = ((null: any): DispatcherContext);
state = ((null: any): StateContext);
});const Capture = () => {
dispatch = React.useContext(TreeContext.TreeDispatcherContext);
state = React.useContext(TreeContext.TreeStateContext);
return null;
};const Contexts = () => {
return (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
<TreeContext.TreeContextController>
<Capture />
</TreeContext.TreeContextController>
</StoreContext.Provider>
</BridgeContext.Provider>
);};describe('tree state', () => {it('should select the next and previous elements in the tree', () => {const Grandparent = () => <Parent />;const Parent = () => (<React.Fragment>
<Child /><Child /></React.Fragment>
);const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>`);// Test stepping through to the endutils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent>→ <Child><Child>`);utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child>→ <Child>`);// Test stepping back to the beginningutils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent>→ <Child><Child>`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>`);// Test wrap around behaviorutils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child>→ <Child>`);utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>`);});it('should select child elements', () => {const Grandparent = () => (<React.Fragment>
<Parent /><Parent /></React.Fragment>
);const Parent = () => (<React.Fragment>
<Child /><Child /></React.Fragment>
);const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent>→ <Child><Child>▾ <Parent><Child><Child>`);// There are no more children to select, so this should be a no-oputils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent>→ <Child><Child>▾ <Parent><Child><Child>`);});it('should select parent elements and then collapse', () => {const Grandparent = () => (<React.Fragment>
<Parent /><Parent /></React.Fragment>
);const Parent = () => (<React.Fragment>
<Child /><Child /></React.Fragment>
);const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
// Select the last childutils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child>→ <Child>`);// Select its parentutils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>→ ▾ <Parent><Child><Child>`);// Select grandparentutils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);// No-oputils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Parent><Child><Child>▾ <Parent><Child><Child>`);const previousState = state;// There are no more ancestors to select, so this should be a no-oputils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toEqual(previousState);
});it('should clear selection if the selected element is unmounted', async () => {const Grandparent = props => props.children || null;
const Parent = props => props.children || null;
const Child = () => null;const container = document.createElement('div');
utils.act(() =>
legacyRender(<Grandparent><Parent><Child /><Child /></Parent></Grandparent>,container,),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>`);// Select the second childutils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child>→ <Child>`);// Remove the child (which should auto-select the parent)await utils.actAsync(() =>
legacyRender(<Grandparent><Parent /></Grandparent>,container,),);expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ <Parent>`);// Unmount the root (so that nothing is selected)await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
expect(state).toMatchInlineSnapshot(``);
});it('should navigate next/previous sibling and skip over children in between', () => {const Grandparent = () => (<React.Fragment>
<Parent numChildren={1} />
<Parent numChildren={3} />
<Parent numChildren={2} />
</React.Fragment>
);const Parent = ({numChildren}) =>new Array(numChildren).fill(true)
.map((_, index) => <Child key={index} />);
const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
const firstParentID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0">→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">→ ▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">→ ▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0">→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Parent><Child key="0"><Child key="1">`);utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0">▾ <Parent><Child key="0"><Child key="1"><Child key="2">→ ▾ <Parent><Child key="0"><Child key="1">`);});it('should navigate the owner hierarchy', () => {const Wrapper = ({children}) => children;const Grandparent = () => (<React.Fragment>
<Wrapper><Parent numChildren={1} />
</Wrapper><Wrapper><Parent numChildren={3} />
</Wrapper><Wrapper><Parent numChildren={2} />
</Wrapper></React.Fragment>
);const Parent = ({numChildren}) =>new Array(numChildren).fill(true)
.map((_, index) => <Child key={index} />);
const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
const childID = ((store.getElementIDAtIndex(7): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0">→ <Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Basic navigation testutils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Noop (since we're at the root already)utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0">→ <Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Noop (since we're at the leaf node)utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0">→ <Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Other navigational actions should clear out the temporary owner chain.
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent>→ <Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Start a new tree on parentconst parentID = ((store.getElementIDAtIndex(5): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: parentID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Noop (since we're at the top)utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);// Noop (since we're at the leaf of this owner tree)// It should not be possible to navigate beyond the owner chain leaf.
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Wrapper>▾ <Parent><Child key="0">▾ <Wrapper>→ ▾ <Parent><Child key="0"><Child key="1"><Child key="2">▾ <Wrapper>▾ <Parent><Child key="0"><Child key="1">`);});});describe('search state', () => {it('should find elements matching search text', () => {const Foo = () => null;const Bar = () => null;const Baz = () => null;const Qux = () => null;Qux.displayName = `withHOC(${Qux.name})`;
utils.act(() =>
legacyRender(<React.Fragment>
<Foo /><Bar /><Baz /><Qux /></React.Fragment>,
document.createElement('div'),
),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar><Baz><Qux> [withHOC]
`);// NOTE: multi-matchutils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar><Baz><Qux> [withHOC]
`);// NOTE: single matchutils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'f'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ <Foo><Bar><Baz><Qux> [withHOC]
`);// NOTE: no matchutils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'y'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
→ <Foo><Bar><Baz><Qux> [withHOC]
`);// NOTE: HOC matchutils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'w'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar><Baz>→ <Qux> [withHOC]
`);});it('should select the next and previous items within the search results', () => {const Foo = () => null;const Bar = () => null;const Baz = () => null;utils.act(() =>
legacyRender(<React.Fragment>
<Foo /><Baz /><Bar /><Baz /></React.Fragment>,
document.createElement('div'),
),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Baz><Bar><Baz>`);// search for "ba"utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Baz><Bar><Baz>`);// go to second resultutils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Baz>→ <Bar><Baz>`);// go to third resultutils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Baz><Bar>→ <Baz>`);// go to second resultutils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Baz>→ <Bar><Baz>`);// go to first resultutils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Baz><Bar><Baz>`);// wrap to last resultutils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Baz><Bar>→ <Baz>`);// wrap to first resultutils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Baz><Bar><Baz>`);});it('should add newly mounted elements to the search results set if they match the current text', async () => {const Foo = () => null;const Bar = () => null;const Baz = () => null;const container = document.createElement('div');
utils.act(() =>
legacyRender(<React.Fragment>
<Foo /><Bar /></React.Fragment>,
container,),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar>`);utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar>`);await utils.actAsync(() =>
legacyRender(<React.Fragment>
<Foo /><Bar /><Baz /></React.Fragment>,
container,),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar><Baz>`);utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar>→ <Baz>`);});it('should remove unmounted elements from the search results set', async () => {const Foo = () => null;const Bar = () => null;const Baz = () => null;const container = document.createElement('div');
utils.act(() =>
legacyRender(<React.Fragment>
<Foo /><Bar /><Baz /></React.Fragment>,
container,),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar><Baz>`);utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar><Baz>`);utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar>→ <Baz>`);await utils.actAsync(() =>
legacyRender(<React.Fragment>
<Foo /><Bar /></React.Fragment>,
container,),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo><Bar>`);utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar>`);// Noop since the list is now one item longutils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Foo>→ <Bar>`);});});describe('owners state', () => {it('should support entering and existing the owners tree view', () => {const Grandparent = () => <Parent />;const Parent = () => (<React.Fragment>
<Child /><Child /></React.Fragment>
);const Child = () => null;utils.act(() =>
legacyRender(<Grandparent />, document.createElement('div')),
);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child><Child>`);const parentID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
→ ▾ <Parent><Child><Child>`);utils.act(() => dispatch({type: 'RESET_OWNER_STACK'}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>→ ▾ <Parent><Child><Child>`);});it('should remove an element from the owners list if it is unmounted', async () => {const Grandparent = ({count}) => <Parent count={count} />;const Parent = ({count}) =>new Array(count).fill(true).map((_, index) => <Child key={index} />);
const Child = () => null;const container = document.createElement('div');
utils.act(() => legacyRender(<Grandparent count={2} />, container));
let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Grandparent>▾ <Parent><Child key="0"><Child key="1">`);const parentID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
→ ▾ <Parent><Child key="0"><Child key="1">`);await utils.actAsync(() =>
legacyRender(<Grandparent count={1} />, container),
);expect(state).toMatchInlineSnapshot(`
[owners]
→ ▾ <Parent><Child key="0">`);await utils.actAsync(() =>
legacyRender(<Grandparent count={0} />, container),
);expect(state).toMatchInlineSnapshot(`
[owners]
→ <Parent>`);});it('should exit the owners list if the current owner is unmounted', async () => {const Parent = props => props.children || null;
const Child = () => null;const container = document.createElement('div');
utils.act(() =>
legacyRender(<Parent><Child /></Parent>,container,),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Parent><Child>`);const childID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
→ <Child>`);await utils.actAsync(() => legacyRender(<Parent />, container));
expect(state).toMatchInlineSnapshot(`
[root]
→ <Parent>`);const parentID = ((store.getElementIDAtIndex(0): any): number);
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
→ <Parent>`);await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
expect(state).toMatchInlineSnapshot(``);
});// This tests ensures support for toggling Suspense boundaries outside of the active owners list.
it('should exit the owners list if an element outside the list is selected', () => {const Grandchild = () => null;const Child = () => (<React.Suspense fallback="Loading">
<Grandchild /></React.Suspense>
);const Parent = () => (<React.Suspense fallback="Loading">
<Child /></React.Suspense>
);const container = document.createElement('div');
utils.act(() => legacyRender(<Parent />, container));
let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Parent>▾ <Suspense>▾ <Child>▾ <Suspense><Grandchild>`);const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
const childID = ((store.getElementIDAtIndex(2): any): number);
const innerSuspenseID = ((store.getElementIDAtIndex(3): any): number);
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
→ ▾ <Child>▾ <Suspense><Grandchild>`);// Toggling a Suspense boundary inside of the flat list should update selected indexutils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: innerSuspenseID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[owners]
▾ <Child>→ ▾ <Suspense><Grandchild>`);// Toggling a Suspense boundary outside of the flat list should exit owners list and update indexutils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: outerSuspenseID}),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
▾ <Parent>→ ▾ <Suspense>▾ <Child>▾ <Suspense><Grandchild>`);});});describe('inline errors/warnings state', () => {const {clearErrorsAndWarnings: clearErrorsAndWarningsAPI,clearErrorsForElement: clearErrorsForElementAPI,clearWarningsForElement: clearWarningsForElementAPI,} = require('react-devtools-shared/src/backendAPI');function clearAllErrors() {utils.act(() => clearErrorsAndWarningsAPI({bridge, store}));
// flush events to the rendererjest.runAllTimers();
}function clearErrorsForElement(id) {const rendererID = store.getRendererIDForElement(id);
utils.act(() => clearErrorsForElementAPI({bridge, id, rendererID}));
// flush events to the rendererjest.runAllTimers();
}function clearWarningsForElement(id) {const rendererID = store.getRendererIDForElement(id);
utils.act(() => clearWarningsForElementAPI({bridge, id, rendererID}));
// flush events to the rendererjest.runAllTimers();
}function selectNextErrorOrWarning() {utils.act(() =>
dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),);}function selectPreviousErrorOrWarning() {utils.act(() =>
dispatch({type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',}),);}function Child({logError = false, logWarning = false}) {if (logError === true) {console.error('test-only: error');
}if (logWarning === true) {console.warn('test-only: warning');
}return null;}it('should handle when there are no errors/warnings', () => {utils.act(() =>
legacyRender(<React.Fragment>
<Child /><Child /><Child /></React.Fragment>,
document.createElement('div'),
),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Child><Child><Child>`);// Next/previous errors should be a no-opselectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
<Child><Child><Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
<Child><Child><Child>`);utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
expect(state).toMatchInlineSnapshot(`
[root]
→ <Child><Child><Child>`);// Next/previous errors should still be a no-opselectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
→ <Child><Child><Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
→ <Child><Child><Child>`);});it('should cycle through the next errors/warnings and wrap around', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} /><Child /><Child logError={true} /><Child /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child><Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠<Child><Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child>→ <Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠<Child><Child> ✕<Child>`);});it('should cycle through the previous errors/warnings and wrap around', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} /><Child /><Child logError={true} /><Child /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child><Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child>→ <Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠<Child><Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child>→ <Child> ✕<Child>`);});it('should cycle through the next errors/warnings and wrap around with multiple roots', () => {withErrorsOrWarningsIgnored(['test-only:'], () => {
utils.act(() => {
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} />,</React.Fragment>,
document.createElement('div'),
);legacyRender(<React.Fragment>
<Child /><Child logError={true} /><Child /></React.Fragment>,
document.createElement('div'),
);});});utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠[root]
<Child><Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠[root]
<Child><Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠[root]
<Child>→ <Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠[root]
<Child><Child> ✕<Child>`);});it('should cycle through the previous errors/warnings and wrap around with multiple roots', () => {withErrorsOrWarningsIgnored(['test-only:'], () => {
utils.act(() => {
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} />,</React.Fragment>,
document.createElement('div'),
);legacyRender(<React.Fragment>
<Child /><Child logError={true} /><Child /></React.Fragment>,
document.createElement('div'),
);});});utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠[root]
<Child><Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠[root]
<Child>→ <Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠[root]
<Child><Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠[root]
<Child>→ <Child> ✕<Child>`);});it('should select the next or previous element relative to the current selection', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} /><Child /><Child logError={true} /><Child /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠→ <Child><Child> ✕<Child>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠<Child>→ <Child> ✕<Child>`);utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child><Child> ⚠→ <Child><Child> ✕<Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child>→ <Child> ⚠<Child><Child> ✕<Child>`);});it('should update correctly when errors/warnings are cleared for a fiber in the list', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child logWarning={true} /><Child logError={true} /><Child logError={true} /><Child logWarning={true} /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]
<Child> ⚠<Child> ✕<Child> ✕<Child> ⚠`);// Select the first item in the listselectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]
→ <Child> ⚠<Child> ✕<Child> ✕<Child> ⚠`);// Clear warnings (but the next Fiber has only errors)clearWarningsForElement(store.getElementIDAtIndex(1));
selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 2[root]
<Child> ⚠→ <Child> ✕<Child> ✕<Child> ⚠`);clearErrorsForElement(store.getElementIDAtIndex(2));
// Should step to the (now) next one in the list.
selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
<Child> ⚠<Child> ✕<Child>→ <Child> ⚠`);// Should skip over the (now) cleared FiberselectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
<Child> ⚠→ <Child> ✕<Child><Child> ⚠`);});it('should update correctly when errors/warnings are cleared for the currently selected fiber', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child logWarning={true} /><Child logError={true} /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child> ⚠<Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
→ <Child> ⚠<Child> ✕`);clearWarningsForElement(store.getElementIDAtIndex(0));
selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<Child>→ <Child> ✕`);});it('should update correctly when new errors/warnings are added', () => {const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child logWarning={true} /><Child /><Child /><Child logError={true} /></React.Fragment>,
container,),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child> ⚠<Child><Child><Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
→ <Child> ⚠<Child><Child><Child> ✕`);withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child /><Child logWarning={true} /><Child /><Child /></React.Fragment>,
container,),),);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
<Child> ⚠→ <Child> ⚠<Child><Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
<Child> ⚠<Child> ⚠<Child>→ <Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 2[root]
→ <Child> ⚠<Child> ⚠<Child><Child> ✕`);});it('should update correctly when all errors/warnings are cleared', () => {withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child logWarning={true} /><Child logError={true} /></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
<Child> ⚠<Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 1[root]
→ <Child> ⚠<Child> ✕`);clearAllErrors();selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
→ <Child><Child>`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
→ <Child><Child>`);});it('should update correctly when elements are added/removed', () => {const container = document.createElement('div');
let errored = false;function ErrorOnce() {if (!errored) {errored = true;console.error('test-only:one-time-error');
}return null;}withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<ErrorOnce key="error" /></React.Fragment>,
container,),),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<ErrorOnce key="error"> ✕`);withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child /><ErrorOnce key="error" /></React.Fragment>,
container,),),);utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<Child><ErrorOnce key="error"> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<Child>→ <ErrorOnce key="error"> ✕`);});it('should update correctly when elements are re-ordered', () => {const container = document.createElement('div');
function ErrorOnce() {const didErrorRef = React.useRef(false);
if (!didErrorRef.current) {
didErrorRef.current = true;
console.error('test-only:one-time-error');
}return null;}withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Child key="A" /><ErrorOnce key="B" /><Child key="C" /><ErrorOnce key="D" /></React.Fragment>,
container,),),);let renderer;utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
<Child key="A"><ErrorOnce key="B"> ✕<Child key="C"><ErrorOnce key="D"> ✕`);// Select a childselectNextErrorOrWarning();utils.act(() => renderer.update(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
<Child key="A">→ <ErrorOnce key="B"> ✕<Child key="C"><ErrorOnce key="D"> ✕`);// Re-order the tree and ensure indices are updated.
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<ErrorOnce key="B" /><Child key="A" /><ErrorOnce key="D" /><Child key="C" /></React.Fragment>,
container,),),);expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
→ <ErrorOnce key="B"> ✕<Child key="A"><ErrorOnce key="D"> ✕<Child key="C">`);// Select the next child and ensure the index doesn't break.
selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
<ErrorOnce key="B"> ✕<Child key="A">→ <ErrorOnce key="D"> ✕<Child key="C">`);// Re-order the tree and ensure indices are updated.
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<ErrorOnce key="D" /><ErrorOnce key="B" /><Child key="A" /><Child key="C" /></React.Fragment>,
container,),),);expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
→ <ErrorOnce key="D"> ✕<ErrorOnce key="B"> ✕<Child key="A"><Child key="C">`);});it('should update select and auto-expand parts components within hidden parts of the tree', () => {const Wrapper = ({children}) => children;store.collapseNodesByDefault = true;
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Wrapper><Child logWarning={true} /></Wrapper><Wrapper><Wrapper><Child logWarning={true} /></Wrapper></Wrapper></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▸ <Wrapper>▸ <Wrapper>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper>→ <Child> ⚠▸ <Wrapper>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper><Child> ⚠▾ <Wrapper>▾ <Wrapper>→ <Child> ⚠`);});it('should properly handle when components filters are updated', () => {const Wrapper = ({children}) => children;withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Wrapper><Child logWarning={true} /></Wrapper><Wrapper><Wrapper><Child logWarning={true} /></Wrapper></Wrapper></React.Fragment>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper><Child> ⚠▾ <Wrapper>▾ <Wrapper><Child> ⚠`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper>→ <Child> ⚠▾ <Wrapper>▾ <Wrapper><Child> ⚠`);utils.act(() => {
store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
});expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
→ <Child> ⚠<Child> ⚠`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
<Child> ⚠→ <Child> ⚠`);utils.act(() => {
store.componentFilters = [];
});expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper><Child> ⚠▾ <Wrapper>▾ <Wrapper>→ <Child> ⚠`);selectPreviousErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper>→ <Child> ⚠▾ <Wrapper>▾ <Wrapper><Child> ⚠`);});it('should preserve errors for fibers even if they are filtered out of the tree initially', () => {const Wrapper = ({children}) => children;withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Fragment>
<Wrapper><Child logWarning={true} /></Wrapper><Wrapper><Wrapper><Child logWarning={true} /></Wrapper></Wrapper></React.Fragment>,
document.createElement('div'),
),),);store.componentFilters = [utils.createDisplayNameFilter('Child')];
utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Wrapper>▾ <Wrapper><Wrapper>`);utils.act(() => {
store.componentFilters = [];
});expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper><Child> ⚠▾ <Wrapper>▾ <Wrapper><Child> ⚠`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2[root]
▾ <Wrapper>→ <Child> ⚠▾ <Wrapper>▾ <Wrapper><Child> ⚠`);});describe('suspense', () => {// This verifies that we don't flush before the tree has been committed.
it('should properly handle errors/warnings from components inside of delayed Suspense', async () => {const NeverResolves = React.lazy(() => new Promise(() => {}));
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Suspense fallback={null}>
<Child logWarning={true} /><NeverResolves /></React.Suspense>,
document.createElement('div'),
),),);utils.act(() => TestRenderer.create(<Contexts />));
jest.runAllTimers();
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>`);});it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {async function fakeImport(result) {return {default: result};}const LazyComponent = React.lazy(() => fakeImport(Child));
const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Suspense fallback={null}>
<Child logWarning={true} /><LazyComponent /></React.Suspense>,
container,),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>`);await Promise.resolve();
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Suspense fallback={null}>
<Child logWarning={true} /><LazyComponent /></React.Suspense>,
container,),),);expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 1[root]
▾ <Suspense><Child> ⚠<Child>`);});it('should properly show errors/warnings from components in the Suspense fallback tree', async () => {async function fakeImport(result) {return {default: result};}const LazyComponent = React.lazy(() => fakeImport(Child));
const Fallback = () => <Child logError={true} />;const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Suspense fallback={<Fallback />}>
<LazyComponent /></React.Suspense>,
container,),),);utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
▾ <Suspense>▾ <Fallback><Child> ✕`);await Promise.resolve();
withErrorsOrWarningsIgnored(['test-only:'], () =>
utils.act(() =>
legacyRender(<React.Suspense fallback={<Fallback />}>
<LazyComponent /></React.Suspense>,
container,),),);expect(state).toMatchInlineSnapshot(`
[root]
▾ <Suspense><Child>`);});});describe('error boundaries', () => {it('should properly handle errors/warnings from components that dont mount because of an error', () => {class ErrorBoundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return null;}return this.props.children;
}}class BadRender extends React.Component {
render() {console.error('test-only: I am about to throw!');
throw new Error('test-only: Oops!');}}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:', 'React will try to recreate this component tree'],
() => {utils.act(() =>
legacyRender(<ErrorBoundary><BadRender /></ErrorBoundary>,container,),);},);utils.act(() => TestRenderer.create(<Contexts />));
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<ErrorBoundary> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
→ <ErrorBoundary> ✕`);utils.act(() => ReactDOM.unmountComponentAtNode(container));
expect(state).toMatchInlineSnapshot(``);
// Should be a noopselectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(``);
});it('should properly handle errors/warnings from components that dont mount because of an error', () => {class ErrorBoundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return null;}return this.props.children;
}}class LogsWarning extends React.Component {
render() {console.warn('test-only: I am about to throw!');
return <ThrowsError />;}}class ThrowsError extends React.Component {
render() {throw new Error('test-only: Oops!');}}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:', 'React will try to recreate this component tree'],
() => {utils.act(() =>
legacyRender(<ErrorBoundary><LogsWarning /></ErrorBoundary>,container,),);},);utils.act(() => TestRenderer.create(<Contexts />));
expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
<ErrorBoundary> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 1, ⚠ 0[root]
→ <ErrorBoundary> ✕`);utils.act(() => ReactDOM.unmountComponentAtNode(container));
expect(state).toMatchInlineSnapshot(``);
// Should be a noopselectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(``);
});it('should properly handle errors/warnings from inside of an error boundary', () => {class ErrorBoundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return <Child logError={true} />;}return this.props.children;
}}class BadRender extends React.Component {
render() {console.error('test-only: I am about to throw!');
throw new Error('test-only: Oops!');}}const container = document.createElement('div');
withErrorsOrWarningsIgnored(['test-only:', 'React will try to recreate this component tree'],
() => {utils.act(() =>
legacyRender(<ErrorBoundary><BadRender /></ErrorBoundary>,container,),);},);utils.act(() => TestRenderer.create(<Contexts />));
expect(store).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
▾ <ErrorBoundary> ✕<Child> ✕`);selectNextErrorOrWarning();expect(state).toMatchInlineSnapshot(`
✕ 2, ⚠ 0[root]
→ ▾ <ErrorBoundary> ✕<Child> ✕`);});});});});