/**
* 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 (
);
};
describe('tree state', () => {
it('should select the next and previous elements in the tree', () => {
const Grandparent = () => ;
const Parent = () => (
);
const Child = () => null;
utils.act(() =>
legacyRender(, document.createElement('div')),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
`);
// Test stepping through to the end
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
`);
// Test stepping back to the beginning
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
`);
// Test wrap around behavior
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
`);
});
it('should select child elements', () => {
const Grandparent = () => (
);
const Parent = () => (
);
const Child = () => null;
utils.act(() =>
legacyRender(, document.createElement('div')),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
▾
`);
// There are no more children to select, so this should be a no-op
utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
▾
`);
});
it('should select parent elements and then collapse', () => {
const Grandparent = () => (
);
const Parent = () => (
);
const Child = () => null;
utils.act(() =>
legacyRender(, document.createElement('div')),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
`);
const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
// Select the last child
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
→
`);
// Select its parent
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→ ▾
`);
// Select grandparent
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
▾
`);
// No-op
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
▾
`);
const previousState = state;
// There are no more ancestors to select, so this should be a no-op
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
utils.act(() => renderer.update());
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(
,
container,
),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
`);
// Select the second child
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→
`);
// Remove the child (which should auto-select the parent)
await utils.actAsync(() =>
legacyRender(
,
container,
),
);
expect(state).toMatchInlineSnapshot(`
[root]
▾
→
`);
// 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 = () => (
);
const Parent = ({numChildren}) =>
new Array(numChildren)
.fill(true)
.map((_, index) => );
const Child = () => null;
utils.act(() =>
legacyRender(, document.createElement('div')),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
const firstParentID = ((store.getElementIDAtIndex(1): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→ ▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
→ ▾
`);
utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
→ ▾
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
→ ▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
→ ▾
▾
▾
`);
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
→ ▾
`);
});
it('should navigate the owner hierarchy', () => {
const Wrapper = ({children}) => children;
const Grandparent = () => (
);
const Parent = ({numChildren}) =>
new Array(numChildren)
.fill(true)
.map((_, index) => );
const Child = () => null;
utils.act(() =>
legacyRender(, document.createElement('div')),
);
let renderer;
utils.act(() => (renderer = TestRenderer.create()));
const childID = ((store.getElementIDAtIndex(7): any): number);
utils.act(() =>
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
▾
→
▾
▾
`);
// Basic navigation test
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾
▾
→ ▾
▾
▾
`);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
▾
▾
▾
▾
▾
`);
// Noop (since we're at the root already)
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
→ ▾
▾
▾
▾
▾
▾
▾
`);
utils.act(() =>
dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
);
utils.act(() => renderer.update());
expect(state).toMatchInlineSnapshot(`
[root]
▾
▾
▾