mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
607 lines
20 KiB
JavaScript
607 lines
20 KiB
JavaScript
// @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 store: Store;
|
||
let utils;
|
||
|
||
let BridgeContext;
|
||
let StoreContext;
|
||
let TreeContext;
|
||
|
||
let dispatch: DispatcherContext;
|
||
let state: StateContext;
|
||
|
||
beforeEach(() => {
|
||
utils = require('./utils');
|
||
utils.beforeEachProfiling();
|
||
|
||
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(() =>
|
||
ReactDOM.render(<Grandparent />, document.createElement('div')),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: select first element');
|
||
|
||
while (
|
||
state.selectedElementIndex !== null &&
|
||
state.selectedElementIndex < store.numElements - 1
|
||
) {
|
||
const index = ((state.selectedElementIndex: any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot(`3: select element after (${index})`);
|
||
}
|
||
|
||
while (
|
||
state.selectedElementIndex !== null &&
|
||
state.selectedElementIndex > 0
|
||
) {
|
||
const index = ((state.selectedElementIndex: any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot(`4: select element before (${index})`);
|
||
}
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('5: select previous wraps around to last');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('6: select next wraps around to first');
|
||
});
|
||
|
||
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(() =>
|
||
ReactDOM.render(<Grandparent />, document.createElement('div')),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: select first element');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: select Parent');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: select Child');
|
||
|
||
const previousState = state;
|
||
|
||
// 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(<Contexts />));
|
||
expect(state).toEqual(previousState);
|
||
});
|
||
|
||
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(() =>
|
||
ReactDOM.render(<Grandparent />, document.createElement('div')),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
|
||
|
||
utils.act(() =>
|
||
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),
|
||
);
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: select last child');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: select Parent');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: select Grandparent');
|
||
|
||
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(<Contexts />));
|
||
expect(state).toEqual(previousState);
|
||
});
|
||
|
||
it('should clear selection if the selected element is unmounted', async done => {
|
||
const Grandparent = props => props.children || null;
|
||
const Parent = props => props.children || null;
|
||
const Child = () => null;
|
||
|
||
const container = document.createElement('div');
|
||
utils.act(() =>
|
||
ReactDOM.render(
|
||
<Grandparent>
|
||
<Parent>
|
||
<Child />
|
||
<Child />
|
||
</Parent>
|
||
</Grandparent>,
|
||
container,
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: select second child');
|
||
|
||
await utils.actAsync(() =>
|
||
ReactDOM.render(
|
||
<Grandparent>
|
||
<Parent />
|
||
</Grandparent>,
|
||
container,
|
||
),
|
||
);
|
||
expect(state).toMatchSnapshot(
|
||
'3: remove children (parent should now be selected)',
|
||
);
|
||
|
||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
|
||
expect(state).toMatchSnapshot(
|
||
'4: unmount root (nothing should be selected)',
|
||
);
|
||
|
||
done();
|
||
});
|
||
});
|
||
|
||
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(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Bar />
|
||
<Baz />
|
||
<Qux />
|
||
</React.Fragment>,
|
||
document.createElement('div'),
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
// NOTE: multi-match
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||
|
||
// NOTE: single match
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'f'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: search for "f"');
|
||
|
||
// NOTE: no match
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'y'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: search for "y"');
|
||
|
||
// NOTE: HOC match
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'w'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('5: search for "w"');
|
||
});
|
||
|
||
it('should select the next and previous items within the search results', () => {
|
||
const Foo = () => null;
|
||
const Bar = () => null;
|
||
const Baz = () => null;
|
||
|
||
utils.act(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Baz />
|
||
<Bar />
|
||
<Baz />
|
||
</React.Fragment>,
|
||
document.createElement('div'),
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: go to second result');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: go to third result');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('5: go to second result');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('6: go to first result');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('7: wrap to last result');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('8: wrap to first result');
|
||
});
|
||
|
||
it('should add newly mounted elements to the search results set if they match the current text', async done => {
|
||
const Foo = () => null;
|
||
const Bar = () => null;
|
||
const Baz = () => null;
|
||
|
||
const container = document.createElement('div');
|
||
|
||
utils.act(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Bar />
|
||
</React.Fragment>,
|
||
container,
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||
|
||
await utils.actAsync(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Bar />
|
||
<Baz />
|
||
</React.Fragment>,
|
||
container,
|
||
),
|
||
);
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: mount Baz');
|
||
|
||
done();
|
||
});
|
||
|
||
it('should remove unmounted elements from the search results set', async done => {
|
||
const Foo = () => null;
|
||
const Bar = () => null;
|
||
const Baz = () => null;
|
||
|
||
const container = document.createElement('div');
|
||
|
||
utils.act(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Bar />
|
||
<Baz />
|
||
</React.Fragment>,
|
||
container,
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||
|
||
utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: go to second result');
|
||
|
||
await utils.actAsync(() =>
|
||
ReactDOM.render(
|
||
<React.Fragment>
|
||
<Foo />
|
||
<Bar />
|
||
</React.Fragment>,
|
||
container,
|
||
),
|
||
);
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: unmount Baz');
|
||
|
||
done();
|
||
});
|
||
});
|
||
|
||
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(() =>
|
||
ReactDOM.render(<Grandparent />, document.createElement('div')),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||
|
||
utils.act(() => dispatch({type: 'RESET_OWNER_STACK'}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: final state');
|
||
});
|
||
|
||
it('should remove an element from the owners list if it is unmounted', async done => {
|
||
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(() => ReactDOM.render(<Grandparent count={2} />, container));
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||
|
||
await utils.actAsync(() =>
|
||
ReactDOM.render(<Grandparent count={1} />, container),
|
||
);
|
||
expect(state).toMatchSnapshot('3: remove second child');
|
||
|
||
await utils.actAsync(() =>
|
||
ReactDOM.render(<Grandparent count={0} />, container),
|
||
);
|
||
expect(state).toMatchSnapshot('4: remove first child');
|
||
|
||
done();
|
||
});
|
||
|
||
it('should exit the owners list if the current owner is unmounted', async done => {
|
||
const Parent = props => props.children || null;
|
||
const Child = () => null;
|
||
|
||
const container = document.createElement('div');
|
||
utils.act(() =>
|
||
ReactDOM.render(
|
||
<Parent>
|
||
<Child />
|
||
</Parent>,
|
||
container,
|
||
),
|
||
);
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
let childID = ((store.getElementIDAtIndex(1): any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('2: child owners tree');
|
||
|
||
await utils.actAsync(() => ReactDOM.render(<Parent />, container));
|
||
expect(state).toMatchSnapshot('3: remove child');
|
||
|
||
let parentID = ((store.getElementIDAtIndex(0): any): number);
|
||
utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: parent owners tree');
|
||
|
||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
|
||
expect(state).toMatchSnapshot('5: unmount root');
|
||
|
||
done();
|
||
});
|
||
|
||
// 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(() => ReactDOM.render(<Parent />, container));
|
||
|
||
expect(store).toMatchSnapshot('0: mount');
|
||
|
||
let renderer;
|
||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||
expect(state).toMatchSnapshot('1: initial state');
|
||
|
||
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).toMatchSnapshot('2: child owners tree');
|
||
|
||
// Toggling a Suspense boundary inside of the flat list should update selected index
|
||
utils.act(() =>
|
||
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: innerSuspenseID}),
|
||
);
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('3: child owners tree');
|
||
|
||
// Toggling a Suspense boundary outside of the flat list should exit owners list and update index
|
||
utils.act(() =>
|
||
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: outerSuspenseID}),
|
||
);
|
||
utils.act(() => renderer.update(<Contexts />));
|
||
expect(state).toMatchSnapshot('4: main tree');
|
||
});
|
||
});
|
||
});
|