@samitouri / QOS-React / commits / 15eacae02c

Moved owners list calculations into the store and added tests

This is being done to fix a drill-through bug, although the initial fix is perhaps not the most performant one. At least we have test coverage now and a temporary fix.

Brian Vaughn committed Apr 24, 2019 at 15:19 UTC 15eacae02c1d526bdc54cbff6e32d068fd483257
7 files changed +192 -118
src/__tests__/__snapshots__/storeOwners-test.js.snap new
+20
@@ -0,0 +1,20 @@
1 +// Jest Snapshot v1, https://goo.gl/fbAQLP
2 +
3 +exports[`Store owners list should drill through intermediate components: 1: mount 1`] = `
4 +[root]
5 + ▾ <Root>
6 + ▾ <Intermediate>
7 + ▾ <Wrapper>
8 + <Leaf key="children">
9 +`;
10 +
11 +exports[`Store owners list should drill through intermediate components: 2: components owned by <Root> 1`] = `
12 +" ▾ <Root>
13 + ▾ <Intermediate>
14 + <Leaf key=\\"children\\">"
15 +`;
16 +
17 +exports[`Store owners list should drill through intermediate components: 3: components owned by <Intermediate> 1`] = `
18 +" ▾ <Intermediate>
19 + ▾ <Wrapper>"
20 +`;
src/__tests__/storeOwners-test.js new
+50
@@ -0,0 +1,50 @@
1 +// @flow
2 +
3 +const { printOwnersList } = require('./storeSerializer');
4 +
5 +describe('Store owners list', () => {
6 + let React;
7 + let ReactDOM;
8 + let TestUtils;
9 + let store;
10 +
11 + const act = (callback: Function) => {
12 + TestUtils.act(() => {
13 + callback();
14 + });
15 + jest.runAllTimers(); // Flush Bridge operations
16 + };
17 +
18 + beforeEach(() => {
19 + store = global.store;
20 + store.collapseNodesByDefault = false;
21 +
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + TestUtils = require('react-dom/test-utils');
25 + });
26 +
27 + it('should drill through intermediate components', () => {
28 + const Root = () => (
29 + <Intermediate>
30 + <Leaf key="children" />
31 + </Intermediate>
32 + );
33 + const Wrapper = ({ children }) => children;
34 + const Leaf = () => <div>Leaf</div>;
35 + const Intermediate = ({ children }) => <Wrapper>{children}</Wrapper>;
36 +
37 + act(() => ReactDOM.render(<Root />, document.createElement('div')));
38 + expect(store).toMatchSnapshot('1: mount');
39 +
40 + const rootID = store.getElementIDAtIndex(0);
41 + expect(
42 + printOwnersList(store.getOwnersListForElement(rootID))
43 + ).toMatchSnapshot('2: components owned by <Root>');
44 +
45 + const intermediateID = store.getElementIDAtIndex(1);
46 + expect(
47 + printOwnersList(store.getOwnersListForElement(intermediateID))
48 + ).toMatchSnapshot('3: components owned by <Intermediate>');
49 + });
50 +});
src/__tests__/storeSerializer.js
+69 -4
@@ -1,9 +1,74 @@
1 import Store from 'src/devtools/store';
2
3 -export function test(value) {
4 - return value instanceof Store;
3 +// test() is part of Jest's serializer API
4 +export function test(maybeStore) {
5 + return maybeStore instanceof Store;
6 }
7
7 -export function print(value, serialize, indent) {
8 - return value.__toSnapshot();
8 +// print() is part of Jest's serializer API
9 +export function print(store, serialize, indent) {
10 + return printStore(store);
11 +}
12 +
13 +export function printElement(element, includeWeight = false) {
14 + let prefix = ' ';
15 + if (element.children.length > 0) {
16 + prefix = element.isCollapsed ? '▸' : '▾';
17 + }
18 +
19 + let key = '';
20 + if (element.key !== null) {
21 + key = ` key="${element.key}"`;
22 + }
23 +
24 + let suffix = '';
25 + if (includeWeight) {
26 + suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
27 + }
28 +
29 + return `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName ||
30 + 'null'}${key}>${suffix}`;
31 +}
32 +
33 +export function printOwnersList(elements, includeWeight = false) {
34 + return elements
35 + .map(element => printElement(element, includeWeight))
36 + .join('\n');
37 +}
38 +
39 +// Used for Jest snapshot testing.
40 +// May also be useful for visually debugging the tree, so it lives on the Store.
41 +export function printStore(store, includeWeight = false) {
42 + const snapshotLines = [];
43 +
44 + let rootWeight = 0;
45 +
46 + store.roots.forEach(rootID => {
47 + const { weight } = store.getElementByID(rootID);
48 +
49 + snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
50 +
51 + for (let i = rootWeight; i < rootWeight + weight; i++) {
52 + const element = store.getElementAtIndex(i);
53 +
54 + if (element == null) {
55 + throw Error(`Could not find element at index ${i}`);
56 + }
57 +
58 + snapshotLines.push(printElement(element, includeWeight));
59 + }
60 +
61 + rootWeight += weight;
62 + });
63 +
64 + // Make sure the pretty-printed test align with the Store's reported number of total rows.
65 + if (rootWeight !== store.numElements) {
66 + throw Error(
67 + `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${
68 + store.numElements
69 + })`
70 + );
71 + }
72 +
73 + return snapshotLines.join('\n');
74 }
src/devtools/store.js
+36 -56
@@ -13,6 +13,7 @@ import { ElementTypeRoot } from './types';
13 import { utfDecodeString } from '../utils';
14 import { __DEBUG__ } from '../constants';
15 import ProfilingCache from './ProfilingCache';
16 +import { printStore } from 'src/__tests__/storeSerializer';
17
18 import type { ElementType } from './types';
19 import type { Element } from './views/Components/types';
@@ -405,6 +406,14 @@ export default class Store extends EventEmitter {
406 return index;
407 }
408
409 + getOwnersListForElement(id: number): Array<Element> {
410 + const list = [];
411 +
412 + this._populateOwnersList(id, id, 0, list);
413 +
414 + return list;
415 + }
416 +
417 getRendererIDForElement(id: number): number | null {
418 let current = this._idToElement.get(id);
419 while (current != null) {
@@ -552,6 +561,32 @@ export default class Store extends EventEmitter {
561 THROTTLE_CAPTURE_SCREENSHOT_DURATION
562 );
563
564 + _populateOwnersList(
565 + id: number,
566 + ownerID: number,
567 + depth: number,
568 + list: Array<Element>
569 + ) {
570 + const element = this._idToElement.get(id);
571 + if (element != null) {
572 + const isInList = id === ownerID || element.ownerID === ownerID;
573 + if (isInList) {
574 + list.push({
575 + ...element,
576 + depth: depth,
577 + });
578 + }
579 + element.children.forEach(childID =>
580 + this._populateOwnersList(
581 + childID,
582 + ownerID,
583 + isInList ? depth + 1 : depth,
584 + list
585 + )
586 + );
587 + }
588 + }
589 +
590 _takeProfilingSnapshotRecursive = (id: number) => {
591 const element = this.getElementByID(id);
592 if (element !== null) {
@@ -870,7 +905,7 @@ export default class Store extends EventEmitter {
905 }
906
907 if (__DEBUG__) {
873 - console.log(this.__toSnapshot(true));
908 + console.log(printStore(this, true));
909 console.groupEnd();
910 }
911
@@ -917,59 +952,4 @@ export default class Store extends EventEmitter {
952 this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
953 this._bridge.removeListener('shutdown', this.onBridgeShutdown);
954 };
920 -
921 - // Used for Jest snapshot testing.
922 - // May also be useful for visually debugging the tree, so it lives on the Store.
923 - __toSnapshot = (includeWeight: boolean = false) => {
924 - const snapshotLines = [];
925 -
926 - let rootWeight = 0;
927 -
928 - this._roots.forEach(rootID => {
929 - const { weight } = ((this.getElementByID(rootID): any): Element);
930 -
931 - snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
932 -
933 - for (let i = rootWeight; i < rootWeight + weight; i++) {
934 - const element = ((this.getElementAtIndex(i): any): Element);
935 -
936 - if (element == null) {
937 - throw Error(`Could not find element at index ${i}`);
938 - }
939 -
940 - let prefix = ' ';
941 - if (element.children.length > 0) {
942 - prefix = element.isCollapsed ? '▸' : '▾';
943 - }
944 -
945 - let key = '';
946 - if (element.key !== null) {
947 - key = ` key="${element.key}"`;
948 - }
949 -
950 - let suffix = '';
951 - if (includeWeight) {
952 - suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
953 - }
954 -
955 - snapshotLines.push(
956 - `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName ||
957 - 'null'}${key}>${suffix}`
958 - );
959 - }
960 -
961 - rootWeight += weight;
962 - });
963 -
964 - // Make sure the pretty-printed test align with the Store's reported number of total rows.
965 - if (rootWeight !== this._weightAcrossRoots) {
966 - throw Error(
967 - `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${
968 - this._weightAcrossRoots
969 - })`
970 - );
971 - }
972 -
973 - return snapshotLines.join('\n');
974 - };
955 }
src/devtools/views/Components/Element.js
+5 -8
@@ -29,17 +29,14 @@ type Props = {
29
30 export default function ElementView({ data, index, style }: Props) {
31 const store = useContext(StoreContext);
32 - const {
33 - baseDepth,
34 - ownerFlatTree,
35 - ownerStack,
36 - selectedElementID,
37 - } = useContext(TreeStateContext);
32 + const { ownerFlatTree, ownerStack, selectedElementID } = useContext(
33 + TreeStateContext
34 + );
35 const dispatch = useContext(TreeDispatcherContext);
36
37 const element =
38 ownerFlatTree !== null
42 - ? store.getElementByID(ownerFlatTree[index])
39 + ? ownerFlatTree[index]
40 : store.getElementAtIndex(index);
41
42 const [isHovered, setIsHovered] = useState(false);
@@ -158,7 +155,7 @@ export default function ElementView({ data, index, style }: Props) {
155 ...style, // "style" comes from react-window
156
157 // Left padding presents the appearance of a nested tree structure.
161 - paddingLeft: `${(depth - baseDepth) * 0.75 + 0.25}rem`,
158 + paddingLeft: `${depth * 0.75 + 0.25}rem`,
159
160 // These style overrides enable the background color to fill the full visible width,
161 // when combined with the CSS tweaks in Tree.
src/devtools/views/Components/Tree.js
-4
@@ -22,7 +22,6 @@ import SearchInput from './SearchInput';
22 import styles from './Tree.css';
23
24 export type ItemData = {|
25 - baseDepth: number,
25 numElements: number,
26 isNavigatingWithKeyboard: boolean,
27 lastScrolledIDRef: { current: number | null },
@@ -35,7 +34,6 @@ type Props = {||};
34 export default function Tree(props: Props) {
35 const dispatch = useContext(TreeDispatcherContext);
36 const {
38 - baseDepth,
37 numElements,
38 ownerStack,
39 searchIndex,
@@ -255,7 +253,6 @@ export default function Tree(props: Props) {
253 // This includes the owner context, since it controls a filtered view of the tree.
254 const itemData = useMemo<ItemData>(
255 () => ({
258 - baseDepth,
256 numElements,
257 isNavigatingWithKeyboard,
258 onElementMouseEnter: handleElementMouseEnter,
@@ -263,7 +260,6 @@ export default function Tree(props: Props) {
260 treeFocused,
261 }),
262 [
266 - baseDepth,
263 numElements,
264 isNavigatingWithKeyboard,
265 handleElementMouseEnter,
src/devtools/views/Components/TreeContext.js
+12 -46
@@ -40,7 +40,6 @@ import type { Element } from './types';
40
41 type StateContext = {|
42 // Tree
43 - baseDepth: number,
43 numElements: number,
44 selectedElementID: number | null,
45 selectedElementIndex: number | null,
@@ -51,7 +50,7 @@ type StateContext = {|
50 searchText: string,
51
52 // Owners
54 - ownerFlatTree: Array<number> | null,
53 + ownerFlatTree: Array<Element> | null,
54 ownerStack: Array<number>,
55 ownerStackIndex: number | null,
56
@@ -133,7 +132,6 @@ TreeDispatcherContext.displayName = 'TreeDispatcherContext';
132
133 type State = {|
134 // Tree
136 - baseDepth: number,
135 numElements: number,
136 selectedElementID: number | null,
137 selectedElementIndex: number | null,
@@ -146,7 +144,7 @@ type State = {|
144 // Owners
145 ownerStack: Array<number>,
146 ownerStackIndex: number | null,
149 - ownerFlatTree: Array<number> | null,
147 + ownerFlatTree: Array<Element> | null,
148
149 // Inspection element panel
150 inspectedElementID: number | null,
@@ -441,7 +439,6 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
439
440 function reduceOwnersState(store: Store, state: State, action: Action): State {
441 let {
444 - baseDepth,
442 numElements,
443 selectedElementID,
444 selectedElementIndex,
@@ -477,7 +474,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
474 }
475 if (selectedElementID !== null && ownerFlatTree !== null) {
476 // Mutation might have caused the index of this ID to shift.
480 - selectedElementIndex = ownerFlatTree.indexOf(selectedElementID);
477 + selectedElementIndex = ownerFlatTree.findIndex(
478 + element => element.id === selectedElementID
479 + );
480 }
481 } else {
482 if (selectedElementID !== null) {
@@ -509,7 +508,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
508 if (ownerFlatTree !== null) {
509 const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
510 selectedElementIndex =
512 - payload === null ? null : ownerFlatTree.indexOf(payload);
511 + payload === null
512 + ? null
513 + : ownerFlatTree.findIndex(element => element.id === payload);
514 }
515 break;
516 case 'SELECT_NEXT_ELEMENT_IN_TREE':
@@ -574,18 +575,11 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
575 ) {
576 if (ownerStackIndex === null) {
577 ownerFlatTree = null;
577 - baseDepth = 0;
578 numElements = store.numElements;
579 } else {
580 - ownerFlatTree = calculateCurrentOwnerList(
581 - store,
582 - ownerStack[ownerStackIndex],
583 - ownerStack[ownerStackIndex],
584 - []
580 + ownerFlatTree = store.getOwnersListForElement(
581 + ownerStack[ownerStackIndex]
582 );
586 -
587 - baseDepth = ((store.getElementByID(ownerFlatTree[0]): any): Element)
588 - .depth;
583 numElements = ownerFlatTree.length;
584 }
585 }
@@ -595,14 +589,14 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
589 if (selectedElementIndex === null) {
590 selectedElementID = null;
591 } else if (ownerFlatTree !== null) {
598 - selectedElementID = ownerFlatTree[((selectedElementIndex: any): number)];
592 + selectedElementID =
593 + ownerFlatTree[((selectedElementIndex: any): number)].id;
594 }
595 }
596
597 return {
598 ...state,
599
605 - baseDepth,
600 numElements,
601 selectedElementID,
602 selectedElementIndex,
@@ -692,7 +686,6 @@ function TreeContextController({ children }: Props) {
686
687 const [state, dispatch] = useReducer(reducer, {
688 // Tree
695 - baseDepth: 0,
689 numElements: store.numElements,
690 selectedElementIndex: null,
691 selectedElementID: null,
@@ -783,33 +776,6 @@ function TreeContextController({ children }: Props) {
776 );
777 }
778
786 -function calculateCurrentOwnerList(
787 - store: Store,
788 - rootOwnerID: number,
789 - elementID: number,
790 - ownerList: Array<number>
791 -): Array<number> {
792 - if (elementID === rootOwnerID) {
793 - ownerList.push(elementID);
794 - const { children } = ((store.getElementByID(elementID): any): Element);
795 - children.forEach(childID =>
796 - calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
797 - );
798 - } else {
799 - const { children, ownerID } = ((store.getElementByID(
800 - elementID
801 - ): any): Element);
802 - if (ownerID === rootOwnerID) {
803 - ownerList.push(elementID);
804 - children.forEach(childID =>
805 - calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
806 - );
807 - }
808 - }
809 -
810 - return ownerList;
811 -}
812 -
779 function recursivelySearchTree(
780 store: Store,
781 elementID: number,