Fixed some edge cases with collapsed by default. Still some bugs existing.
Brian Vaughn committed
Apr 17, 2019 at 07:45 UTC
178e89927a4f5322311a1f00ca0e771bd7d358ca
5 files changed
+199
-43
src/__tests__/__snapshots__/store-test.js.snap
+54
-9
@@ -179,19 +179,62 @@ exports[`Store collapseNodesByDefault:true should filter DOM nodes from the stor
179
▸ <Parent>
180
`;
181
182
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 1: mount 1`] = `
182
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 1: mount 1`] = `
183
+[root]
184
+ ▸ <Wrapper>
185
+`;
186
+
187
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 2: expand deepest node 1`] = `
188
+[root]
189
+ ▾ <Wrapper>
190
+ ▾ <Nested>
191
+ ▾ <Nested>
192
+ ▾ <Nested>
193
+ <Nested>
194
+`;
195
+
196
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 3: collapse root 1`] = `
197
+[root]
198
+ ▸ <Wrapper>
199
+`;
200
+
201
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 4: expand root 1`] = `
202
+[root]
203
+ ▾ <Wrapper>
204
+ ▾ <Nested>
205
+ ▾ <Nested>
206
+ ▾ <Nested>
207
+ <Nested>
208
+`;
209
+
210
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 5: collapse middle node 1`] = `
211
+[root]
212
+ ▾ <Wrapper>
213
+ ▸ <Nested>
214
+`;
215
+
216
+exports[`Store collapseNodesByDefault:true should support expanding deep parts of the tree: 6: expand middle node 1`] = `
217
+[root]
218
+ ▾ <Wrapper>
219
+ ▾ <Nested>
220
+ ▾ <Nested>
221
+ ▾ <Nested>
222
+ <Nested>
223
+`;
224
+
225
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 1: mount 1`] = `
226
[root]
227
▸ <Grandparent>
228
`;
229
187
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 2: expand Grandparent 1`] = `
230
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 2: expand Grandparent 1`] = `
231
[root]
232
▾ <Grandparent>
233
▸ <Parent>
234
▸ <Parent>
235
`;
236
194
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 3: expand first Parent 1`] = `
237
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 3: expand first Parent 1`] = `
238
[root]
239
▾ <Grandparent>
240
▾ <Parent>
@@ -200,7 +243,7 @@ exports[`Store collapseNodesByDefault:true should support collapsing parts of th
243
▸ <Parent>
244
`;
245
203
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 4: expand second Parent 1`] = `
246
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 4: expand second Parent 1`] = `
247
[root]
248
▾ <Grandparent>
249
▾ <Parent>
@@ -211,7 +254,7 @@ exports[`Store collapseNodesByDefault:true should support collapsing parts of th
254
<Child key="1">
255
`;
256
214
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 5: collapse first Parent 1`] = `
257
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 5: collapse first Parent 1`] = `
258
[root]
259
▾ <Grandparent>
260
▸ <Parent>
@@ -220,14 +263,14 @@ exports[`Store collapseNodesByDefault:true should support collapsing parts of th
263
<Child key="1">
264
`;
265
223
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 6: collapse second Parent 1`] = `
266
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 6: collapse second Parent 1`] = `
267
[root]
268
▾ <Grandparent>
269
▸ <Parent>
270
▸ <Parent>
271
`;
272
230
-exports[`Store collapseNodesByDefault:true should support collapsing parts of the tree: 7: collapse Grandparent 1`] = `
273
+exports[`Store collapseNodesByDefault:true should support expanding parts of the tree: 7: collapse Grandparent 1`] = `
274
[root]
275
▸ <Grandparent>
276
`;
@@ -255,12 +298,14 @@ exports[`Store collapseNodesByDefault:true should support mount and update opera
298
299
exports[`Store collapseNodesByDefault:true should support mount and update operations: 1: mount 1`] = `
300
[root]
258
- ▸ <Grandparent>
301
+ ▸ <Parent>
302
+ ▸ <Parent>
303
`;
304
305
exports[`Store collapseNodesByDefault:true should support mount and update operations: 2: update 1`] = `
306
[root]
263
- ▸ <Grandparent>
307
+ ▸ <Parent>
308
+ ▸ <Parent>
309
`;
310
311
exports[`Store collapseNodesByDefault:true should support mount and update operations: 3: unmount 1`] = ``;
src/__tests__/setupTests.js
+4
-1
@@ -31,8 +31,11 @@ env.beforeEach(() => {
31
const agent = new Agent();
32
agent.addBridge(bridge);
33
34
- initBackend(global.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent, global);
34
+ const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
35
36
+ initBackend(hook, agent, global);
37
+
38
+ global.agent = agent;
39
global.bridge = bridge;
40
global.store = new Store(bridge);
41
});
src/__tests__/store-test.js
+62
-9
@@ -195,22 +195,32 @@ describe('Store', () => {
195
});
196
197
it('should support mount and update operations', () => {
198
- const Grandparent = ({ count }) => (
199
- <React.Fragment>
200
- <Parent count={count} />
201
- <Parent count={count} />
202
- </React.Fragment>
203
- );
198
const Parent = ({ count }) =>
199
new Array(count).fill(true).map((_, index) => <Child key={index} />);
200
const Child = () => <div>Hi!</div>;
201
202
const container = document.createElement('div');
203
210
- act(() => ReactDOM.render(<Grandparent count={4} />, container));
204
+ act(() =>
205
+ ReactDOM.render(
206
+ <React.Fragment>
207
+ <Parent count={1} />
208
+ <Parent count={3} />
209
+ </React.Fragment>,
210
+ container
211
+ )
212
+ );
213
expect(store).toMatchSnapshot('1: mount');
214
213
- act(() => ReactDOM.render(<Grandparent count={2} />, container));
215
+ act(() =>
216
+ ReactDOM.render(
217
+ <React.Fragment>
218
+ <Parent count={2} />
219
+ <Parent count={1} />
220
+ </React.Fragment>,
221
+ container
222
+ )
223
+ );
224
expect(store).toMatchSnapshot('2: update');
225
226
act(() => ReactDOM.unmountComponentAtNode(container));
@@ -311,7 +321,7 @@ describe('Store', () => {
321
expect(store).toMatchSnapshot('2: resolved');
322
});
323
314
- it('should support collapsing parts of the tree', () => {
324
+ it('should support expanding parts of the tree', () => {
325
const Grandparent = ({ count }) => (
326
<React.Fragment>
327
<Parent count={count} />
@@ -353,5 +363,48 @@ describe('Store', () => {
363
act(() => store.toggleIsCollapsed(grandparentID, true));
364
expect(store).toMatchSnapshot('7: collapse Grandparent');
365
});
366
+
367
+ it('should support expanding deep parts of the tree', () => {
368
+ const Wrapper = ({ forwardedRef }) => (
369
+ <Nested depth={3} forwardedRef={forwardedRef} />
370
+ );
371
+ const Nested = ({ depth, forwardedRef }) =>
372
+ depth > 0 ? (
373
+ <Nested depth={depth - 1} forwardedRef={forwardedRef} />
374
+ ) : (
375
+ <div ref={forwardedRef} />
376
+ );
377
+
378
+ const ref = React.createRef();
379
+
380
+ act(() =>
381
+ ReactDOM.render(
382
+ <Wrapper forwardedRef={ref} />,
383
+ document.createElement('div')
384
+ )
385
+ );
386
+ expect(store).toMatchSnapshot('1: mount');
387
+
388
+ const deepestedNodeID = global.agent.getIDForNode(ref.current);
389
+
390
+ act(() => store.toggleIsCollapsed(deepestedNodeID, false));
391
+ expect(store).toMatchSnapshot('2: expand deepest node');
392
+
393
+ const rootID = store.getElementIDAtIndex(0);
394
+
395
+ act(() => store.toggleIsCollapsed(rootID, true));
396
+ expect(store).toMatchSnapshot('3: collapse root');
397
+
398
+ act(() => store.toggleIsCollapsed(rootID, false));
399
+ expect(store).toMatchSnapshot('4: expand root');
400
+
401
+ const id = store.getElementIDAtIndex(1);
402
+
403
+ act(() => store.toggleIsCollapsed(id, true));
404
+ expect(store).toMatchSnapshot('5: collapse middle node');
405
+
406
+ act(() => store.toggleIsCollapsed(id, false));
407
+ expect(store).toMatchSnapshot('6: expand middle node');
408
+ });
409
});
410
});
src/devtools/store.js
+77
-19
@@ -315,9 +315,8 @@ export default class Store extends EventEmitter {
315
316
// Find the element in the tree using the weight of each node...
317
// Skip over the root itself, because roots aren't visible in the Elements tree.
318
- const firstChildID = ((root: any): Element).children[0];
319
- let currentElement = ((this._idToElement.get(firstChildID): any): Element);
320
- let currentWeight = rootWeight;
318
+ let currentElement = ((root: any): Element);
319
+ let currentWeight = rootWeight - 1;
320
while (index !== currentWeight) {
321
const numChildren = currentElement.children.length;
322
for (let i = 0; i < numChildren; i++) {
@@ -449,26 +448,67 @@ export default class Store extends EventEmitter {
448
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
449
}
450
451
+ // TODO Maybe split this into two methods: expand() and collapse()
452
toggleIsCollapsed(id: number, isCollapsed: boolean): void {
453
const element = this.getElementByID(id);
454
if (element !== null) {
455
- if (element.type === ElementTypeRoot) {
456
- throw Error('Root nodes cannot be collapsed');
457
- }
455
+ if (isCollapsed) {
456
+ if (element.type === ElementTypeRoot) {
457
+ throw Error('Root nodes cannot be collapsed');
458
+ }
459
+
460
+ if (element.isCollapsed) {
461
+ return;
462
+ }
463
459
- const oldWeight = element.isCollapsed ? 1 : element.weight;
460
- element.isCollapsed = isCollapsed;
461
- const newWeight = element.isCollapsed ? 1 : element.weight;
462
- const weightDelta = newWeight - oldWeight;
464
+ element.isCollapsed = true;
465
464
- this._weightAcrossRoots += weightDelta;
466
+ const weightDelta = 1 - element.weight;
467
466
- let parentElement = this._idToElement.get(element.parentID);
467
- while (parentElement != null) {
468
- parentElement.weight += weightDelta;
469
- parentElement = this._idToElement.get(parentElement.parentID);
468
+ let parentElement = ((this._idToElement.get(
469
+ element.parentID
470
+ ): any): Element);
471
+ while (parentElement != null) {
472
+ parentElement.weight += weightDelta;
473
+ parentElement = this._idToElement.get(parentElement.parentID);
474
+ }
475
+ } else {
476
+ let currentElement = element;
477
+ while (currentElement != null) {
478
+ const oldWeight = currentElement.isCollapsed
479
+ ? 1
480
+ : currentElement.weight;
481
+ currentElement.isCollapsed = false;
482
+ const newWeight = currentElement.isCollapsed
483
+ ? 1
484
+ : currentElement.weight;
485
+ const weightDelta = newWeight - oldWeight;
486
+
487
+ let parentElement = ((this._idToElement.get(
488
+ currentElement.parentID
489
+ ): any): Element);
490
+ while (parentElement != null) {
491
+ parentElement.weight += weightDelta;
492
+ if (parentElement.isCollapsed) {
493
+ break;
494
+ }
495
+ parentElement = this._idToElement.get(parentElement.parentID);
496
+ }
497
+
498
+ currentElement =
499
+ currentElement.parentID !== 0
500
+ ? this.getElementByID(currentElement.parentID)
501
+ : null;
502
+ }
503
}
504
505
+ let weightAcrossRoots = 0;
506
+ this._roots.forEach(rootID => {
507
+ const { weight } = ((this.getElementByID(rootID): any): Element);
508
+ weightAcrossRoots += weight;
509
+ });
510
+ this._weightAcrossRoots = weightAcrossRoots;
511
+
512
// The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
513
// In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
514
// Updating the selected search index later may require auto-expanding a collapsed subtree though.
@@ -867,19 +907,23 @@ export default class Store extends EventEmitter {
907
908
// Used for Jest snapshot testing.
909
// May also be useful for visually debugging the tree, so it lives on the Store.
870
- __toSnapshot = () => {
910
+ __toSnapshot = (includeWeight: boolean = false) => {
911
const snapshotLines = [];
912
913
let rootWeight = 0;
914
915
this._roots.forEach(rootID => {
876
- snapshotLines.push('[root]');
877
-
916
const { weight } = ((this.getElementByID(rootID): any): Element);
917
918
+ snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
919
+
920
for (let i = rootWeight; i < rootWeight + weight; i++) {
921
const element = ((this.getElementAtIndex(i): any): Element);
922
923
+ if (element == null) {
924
+ throw Error(`No element for index ${i}`);
925
+ }
926
+
927
let prefix = ' ';
928
if (element.children.length > 0) {
929
prefix = element.isCollapsed ? '▸' : '▾';
@@ -890,15 +934,29 @@ export default class Store extends EventEmitter {
934
key = ` key="${element.key}"`;
935
}
936
937
+ let suffix = '';
938
+ if (includeWeight) {
939
+ suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
940
+ }
941
+
942
snapshotLines.push(
943
`${' '.repeat(element.depth + 1)}${prefix} <${element.displayName ||
895
- 'null'}${key}>`
944
+ 'null'}${key}>${suffix}`
945
);
946
}
947
948
rootWeight += weight;
949
});
950
951
+ // Make sure the pretty-printed test align with the Store's reported number of total rows.
952
+ if (rootWeight !== this._weightAcrossRoots) {
953
+ throw Error(
954
+ `Inconsistent store state. Individual root weights (${rootWeight}) do not match total weight (${
955
+ this._weightAcrossRoots
956
+ })`
957
+ );
958
+ }
959
+
960
return snapshotLines.join('\n');
961
};
962
}
src/devtools/views/Components/TreeContext.js
+2
-5
@@ -748,11 +748,8 @@ function TreeContextController({ children, viewElementSource }: Props) {
748
749
if (state.selectedElementID !== null) {
750
let element = store.getElementByID(state.selectedElementID);
751
- while (element !== null && element.parentID > 0) {
752
- element = ((store.getElementByID(element.parentID): any): Element);
753
- if (element.isCollapsed) {
754
- store.toggleIsCollapsed(element.id, false);
755
- }
751
+ if (element !== null && element.parentID > 0) {
752
+ store.toggleIsCollapsed(element.parentID, false);
753
}
754
}
755
}