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,