main
js 2,819 lines 76.6 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import typeof ReactTestRenderer from 'react-test-renderer';
11 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
12 import type Store from 'react-devtools-shared/src/devtools/store';
13 import type {
14 DispatcherContext,
15 StateContext,
16 } from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
17
18 import {getVersionedRenderImplementation} from './utils';
19
20 describe('TreeListContext', () => {
21 let React;
22 let TestRenderer: ReactTestRenderer;
23 let bridge: FrontendBridge;
24 let store: Store;
25 let utils;
26 let withErrorsOrWarningsIgnored;
27
28 let BridgeContext;
29 let StoreContext;
30 let TreeContext;
31
32 let dispatch: DispatcherContext;
33 let state: StateContext;
34
35 beforeEach(() => {
36 global.IS_REACT_ACT_ENVIRONMENT = true;
37
38 utils = require('./utils');
39 utils.beforeEachProfiling();
40
41 withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
42
43 bridge = global.bridge;
44 store = global.store;
45 store.collapseNodesByDefault = false;
46
47 React = require('react');
48 TestRenderer = utils.requireTestRenderer();
49
50 BridgeContext =
51 require('react-devtools-shared/src/devtools/views/context').BridgeContext;
52 StoreContext =
53 require('react-devtools-shared/src/devtools/views/context').StoreContext;
54 TreeContext = require('react-devtools-shared/src/devtools/views/Components/TreeContext');
55 });
56
57 const {render, unmount, createContainer} = getVersionedRenderImplementation();
58
59 afterEach(() => {
60 // Reset between tests
61 dispatch = ((null: any): DispatcherContext);
62 state = ((null: any): StateContext);
63 });
64
65 const Capture = () => {
66 dispatch = React.useContext(TreeContext.TreeDispatcherContext);
67 state = React.useContext(TreeContext.TreeStateContext);
68 return null;
69 };
70
71 const Contexts = () => {
72 return (
73 <BridgeContext.Provider value={bridge}>
74 <StoreContext.Provider value={store}>
75 <TreeContext.TreeContextController>
76 <Capture />
77 </TreeContext.TreeContextController>
78 </StoreContext.Provider>
79 </BridgeContext.Provider>
80 );
81 };
82
83 describe('tree state', () => {
84 it('should select the next and previous elements in the tree', () => {
85 const Grandparent = () => <Parent />;
86 const Parent = () => (
87 <React.Fragment>
88 <Child />
89 <Child />
90 </React.Fragment>
91 );
92 const Child = () => null;
93
94 utils.act(() => render(<Grandparent />));
95
96 let renderer;
97 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
98 expect(state).toMatchInlineSnapshot(`
99 [root]
100 ▾ <Grandparent>
101 ▾ <Parent>
102 <Child>
103 <Child>
104 `);
105
106 // Test stepping through to the end
107
108 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
109 utils.act(() => renderer.update(<Contexts />));
110 expect(state).toMatchInlineSnapshot(`
111 [root]
112 → ▾ <Grandparent>
113 ▾ <Parent>
114 <Child>
115 <Child>
116 `);
117
118 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
119 utils.act(() => renderer.update(<Contexts />));
120 expect(state).toMatchInlineSnapshot(`
121 [root]
122 ▾ <Grandparent>
123 → ▾ <Parent>
124 <Child>
125 <Child>
126 `);
127
128 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
129 utils.act(() => renderer.update(<Contexts />));
130 expect(state).toMatchInlineSnapshot(`
131 [root]
132 ▾ <Grandparent>
133 ▾ <Parent>
134 → <Child>
135 <Child>
136 `);
137
138 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
139 utils.act(() => renderer.update(<Contexts />));
140 expect(state).toMatchInlineSnapshot(`
141 [root]
142 ▾ <Grandparent>
143 ▾ <Parent>
144 <Child>
145 → <Child>
146 `);
147
148 // Test stepping back to the beginning
149
150 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
151 utils.act(() => renderer.update(<Contexts />));
152 expect(state).toMatchInlineSnapshot(`
153 [root]
154 ▾ <Grandparent>
155 ▾ <Parent>
156 → <Child>
157 <Child>
158 `);
159
160 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
161 utils.act(() => renderer.update(<Contexts />));
162 expect(state).toMatchInlineSnapshot(`
163 [root]
164 ▾ <Grandparent>
165 → ▾ <Parent>
166 <Child>
167 <Child>
168 `);
169
170 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
171 utils.act(() => renderer.update(<Contexts />));
172 expect(state).toMatchInlineSnapshot(`
173 [root]
174 → ▾ <Grandparent>
175 ▾ <Parent>
176 <Child>
177 <Child>
178 `);
179
180 // Test wrap around behavior
181
182 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
183 utils.act(() => renderer.update(<Contexts />));
184 expect(state).toMatchInlineSnapshot(`
185 [root]
186 ▾ <Grandparent>
187 ▾ <Parent>
188 <Child>
189 → <Child>
190 `);
191
192 utils.act(() => dispatch({type: 'SELECT_NEXT_ELEMENT_IN_TREE'}));
193 utils.act(() => renderer.update(<Contexts />));
194 expect(state).toMatchInlineSnapshot(`
195 [root]
196 → ▾ <Grandparent>
197 ▾ <Parent>
198 <Child>
199 <Child>
200 `);
201 });
202
203 it('should select child elements', () => {
204 const Grandparent = () => (
205 <React.Fragment>
206 <Parent />
207 <Parent />
208 </React.Fragment>
209 );
210 const Parent = () => (
211 <React.Fragment>
212 <Child />
213 <Child />
214 </React.Fragment>
215 );
216 const Child = () => null;
217
218 utils.act(() => render(<Grandparent />));
219
220 let renderer;
221 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
222 expect(state).toMatchInlineSnapshot(`
223 [root]
224 ▾ <Grandparent>
225 ▾ <Parent>
226 <Child>
227 <Child>
228 ▾ <Parent>
229 <Child>
230 <Child>
231 `);
232
233 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
234 utils.act(() => renderer.update(<Contexts />));
235 expect(state).toMatchInlineSnapshot(`
236 [root]
237 → ▾ <Grandparent>
238 ▾ <Parent>
239 <Child>
240 <Child>
241 ▾ <Parent>
242 <Child>
243 <Child>
244 `);
245
246 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
247 utils.act(() => renderer.update(<Contexts />));
248 expect(state).toMatchInlineSnapshot(`
249 [root]
250 ▾ <Grandparent>
251 → ▾ <Parent>
252 <Child>
253 <Child>
254 ▾ <Parent>
255 <Child>
256 <Child>
257 `);
258
259 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
260 utils.act(() => renderer.update(<Contexts />));
261 expect(state).toMatchInlineSnapshot(`
262 [root]
263 ▾ <Grandparent>
264 ▾ <Parent>
265 → <Child>
266 <Child>
267 ▾ <Parent>
268 <Child>
269 <Child>
270 `);
271
272 // There are no more children to select, so this should be a no-op
273 utils.act(() => dispatch({type: 'SELECT_CHILD_ELEMENT_IN_TREE'}));
274 utils.act(() => renderer.update(<Contexts />));
275 expect(state).toMatchInlineSnapshot(`
276 [root]
277 ▾ <Grandparent>
278 ▾ <Parent>
279 → <Child>
280 <Child>
281 ▾ <Parent>
282 <Child>
283 <Child>
284 `);
285 });
286
287 it('should select parent elements and then collapse', () => {
288 const Grandparent = () => (
289 <React.Fragment>
290 <Parent />
291 <Parent />
292 </React.Fragment>
293 );
294 const Parent = () => (
295 <React.Fragment>
296 <Child />
297 <Child />
298 </React.Fragment>
299 );
300 const Child = () => null;
301
302 utils.act(() => render(<Grandparent />));
303
304 let renderer;
305 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
306 expect(state).toMatchInlineSnapshot(`
307 [root]
308 ▾ <Grandparent>
309 ▾ <Parent>
310 <Child>
311 <Child>
312 ▾ <Parent>
313 <Child>
314 <Child>
315 `);
316
317 const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
318
319 // Select the last child
320 utils.act(() =>
321 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID}),
322 );
323 utils.act(() => renderer.update(<Contexts />));
324 expect(state).toMatchInlineSnapshot(`
325 [root]
326 ▾ <Grandparent>
327 ▾ <Parent>
328 <Child>
329 <Child>
330 ▾ <Parent>
331 <Child>
332 → <Child>
333 `);
334
335 // Select its parent
336 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
337 utils.act(() => renderer.update(<Contexts />));
338 expect(state).toMatchInlineSnapshot(`
339 [root]
340 ▾ <Grandparent>
341 ▾ <Parent>
342 <Child>
343 <Child>
344 → ▾ <Parent>
345 <Child>
346 <Child>
347 `);
348
349 // Select grandparent
350 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
351 utils.act(() => renderer.update(<Contexts />));
352 expect(state).toMatchInlineSnapshot(`
353 [root]
354 → ▾ <Grandparent>
355 ▾ <Parent>
356 <Child>
357 <Child>
358 ▾ <Parent>
359 <Child>
360 <Child>
361 `);
362
363 // No-op
364 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
365 utils.act(() => renderer.update(<Contexts />));
366 expect(state).toMatchInlineSnapshot(`
367 [root]
368 → ▾ <Grandparent>
369 ▾ <Parent>
370 <Child>
371 <Child>
372 ▾ <Parent>
373 <Child>
374 <Child>
375 `);
376
377 const previousState = state;
378
379 // There are no more ancestors to select, so this should be a no-op
380 utils.act(() => dispatch({type: 'SELECT_PARENT_ELEMENT_IN_TREE'}));
381 utils.act(() => renderer.update(<Contexts />));
382 expect(state).toEqual(previousState);
383 });
384
385 it('should clear selection if the selected element is unmounted', async () => {
386 const Grandparent = props => props.children || null;
387 const Parent = props => props.children || null;
388 const Child = () => null;
389
390 utils.act(() =>
391 render(
392 <Grandparent>
393 <Parent>
394 <Child />
395 <Child />
396 </Parent>
397 </Grandparent>,
398 ),
399 );
400
401 let renderer;
402 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
403 expect(state).toMatchInlineSnapshot(`
404 [root]
405 ▾ <Grandparent>
406 ▾ <Parent>
407 <Child>
408 <Child>
409 `);
410
411 // Select the second child
412 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 3}));
413 utils.act(() => renderer.update(<Contexts />));
414 expect(state).toMatchInlineSnapshot(`
415 [root]
416 ▾ <Grandparent>
417 ▾ <Parent>
418 <Child>
419 → <Child>
420 `);
421
422 // Remove the child (which should auto-select the parent)
423 await utils.actAsync(() =>
424 render(
425 <Grandparent>
426 <Parent />
427 </Grandparent>,
428 ),
429 );
430 expect(state).toMatchInlineSnapshot(`
431 [root]
432 ▾ <Grandparent>
433 → <Parent>
434 `);
435
436 // Unmount the root (so that nothing is selected)
437 await utils.actAsync(() => unmount());
438 expect(state).toMatchInlineSnapshot(``);
439 });
440
441 it('should navigate next/previous sibling and skip over children in between', () => {
442 const Grandparent = () => (
443 <React.Fragment>
444 <Parent numChildren={1} />
445 <Parent numChildren={3} />
446 <Parent numChildren={2} />
447 </React.Fragment>
448 );
449 const Parent = ({numChildren}) =>
450 new Array(numChildren)
451 .fill(true)
452 .map((_, index) => <Child key={index} />);
453 const Child = () => null;
454
455 utils.act(() => render(<Grandparent />));
456
457 let renderer;
458 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
459
460 const firstParentID = ((store.getElementIDAtIndex(1): any): number);
461
462 utils.act(() =>
463 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: firstParentID}),
464 );
465 utils.act(() => renderer.update(<Contexts />));
466 expect(state).toMatchInlineSnapshot(`
467 [root]
468 ▾ <Grandparent>
469 → ▾ <Parent>
470 <Child key="0">
471 ▾ <Parent>
472 <Child key="0">
473 <Child key="1">
474 <Child key="2">
475 ▾ <Parent>
476 <Child key="0">
477 <Child key="1">
478 `);
479
480 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
481 utils.act(() => renderer.update(<Contexts />));
482 expect(state).toMatchInlineSnapshot(`
483 [root]
484 ▾ <Grandparent>
485 ▾ <Parent>
486 <Child key="0">
487 → ▾ <Parent>
488 <Child key="0">
489 <Child key="1">
490 <Child key="2">
491 ▾ <Parent>
492 <Child key="0">
493 <Child key="1">
494 `);
495
496 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
497 utils.act(() => renderer.update(<Contexts />));
498 expect(state).toMatchInlineSnapshot(`
499 [root]
500 ▾ <Grandparent>
501 ▾ <Parent>
502 <Child key="0">
503 ▾ <Parent>
504 <Child key="0">
505 <Child key="1">
506 <Child key="2">
507 → ▾ <Parent>
508 <Child key="0">
509 <Child key="1">
510 `);
511
512 utils.act(() => dispatch({type: 'SELECT_NEXT_SIBLING_IN_TREE'}));
513 utils.act(() => renderer.update(<Contexts />));
514 expect(state).toMatchInlineSnapshot(`
515 [root]
516 ▾ <Grandparent>
517 → ▾ <Parent>
518 <Child key="0">
519 ▾ <Parent>
520 <Child key="0">
521 <Child key="1">
522 <Child key="2">
523 ▾ <Parent>
524 <Child key="0">
525 <Child key="1">
526 `);
527
528 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
529 utils.act(() => renderer.update(<Contexts />));
530 expect(state).toMatchInlineSnapshot(`
531 [root]
532 ▾ <Grandparent>
533 ▾ <Parent>
534 <Child key="0">
535 ▾ <Parent>
536 <Child key="0">
537 <Child key="1">
538 <Child key="2">
539 → ▾ <Parent>
540 <Child key="0">
541 <Child key="1">
542 `);
543
544 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
545 utils.act(() => renderer.update(<Contexts />));
546 expect(state).toMatchInlineSnapshot(`
547 [root]
548 ▾ <Grandparent>
549 ▾ <Parent>
550 <Child key="0">
551 → ▾ <Parent>
552 <Child key="0">
553 <Child key="1">
554 <Child key="2">
555 ▾ <Parent>
556 <Child key="0">
557 <Child key="1">
558 `);
559
560 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
561 utils.act(() => renderer.update(<Contexts />));
562 expect(state).toMatchInlineSnapshot(`
563 [root]
564 ▾ <Grandparent>
565 → ▾ <Parent>
566 <Child key="0">
567 ▾ <Parent>
568 <Child key="0">
569 <Child key="1">
570 <Child key="2">
571 ▾ <Parent>
572 <Child key="0">
573 <Child key="1">
574 `);
575
576 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_SIBLING_IN_TREE'}));
577 utils.act(() => renderer.update(<Contexts />));
578 expect(state).toMatchInlineSnapshot(`
579 [root]
580 ▾ <Grandparent>
581 ▾ <Parent>
582 <Child key="0">
583 ▾ <Parent>
584 <Child key="0">
585 <Child key="1">
586 <Child key="2">
587 → ▾ <Parent>
588 <Child key="0">
589 <Child key="1">
590 `);
591 });
592
593 it('should navigate the owner hierarchy', () => {
594 const Wrapper = ({children}) => children;
595 const Grandparent = () => (
596 <React.Fragment>
597 <Wrapper>
598 <Parent numChildren={1} />
599 </Wrapper>
600 <Wrapper>
601 <Parent numChildren={3} />
602 </Wrapper>
603 <Wrapper>
604 <Parent numChildren={2} />
605 </Wrapper>
606 </React.Fragment>
607 );
608 const Parent = ({numChildren}) =>
609 new Array(numChildren)
610 .fill(true)
611 .map((_, index) => <Child key={index} />);
612 const Child = () => null;
613
614 utils.act(() => render(<Grandparent />));
615
616 let renderer;
617 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
618
619 const childID = ((store.getElementIDAtIndex(7): any): number);
620 utils.act(() =>
621 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: childID}),
622 );
623 utils.act(() => renderer.update(<Contexts />));
624 expect(state).toMatchInlineSnapshot(`
625 [root]
626 ▾ <Grandparent>
627 ▾ <Wrapper>
628 ▾ <Parent>
629 <Child key="0">
630 ▾ <Wrapper>
631 ▾ <Parent>
632 <Child key="0">
633 → <Child key="1">
634 <Child key="2">
635 ▾ <Wrapper>
636 ▾ <Parent>
637 <Child key="0">
638 <Child key="1">
639 `);
640
641 // Basic navigation test
642 utils.act(() =>
643 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
644 );
645 utils.act(() => renderer.update(<Contexts />));
646 expect(state).toMatchInlineSnapshot(`
647 [root]
648 ▾ <Grandparent>
649 ▾ <Wrapper>
650 ▾ <Parent>
651 <Child key="0">
652 ▾ <Wrapper>
653 → ▾ <Parent>
654 <Child key="0">
655 <Child key="1">
656 <Child key="2">
657 ▾ <Wrapper>
658 ▾ <Parent>
659 <Child key="0">
660 <Child key="1">
661 `);
662
663 utils.act(() =>
664 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
665 );
666 utils.act(() => renderer.update(<Contexts />));
667 expect(state).toMatchInlineSnapshot(`
668 [root]
669 → ▾ <Grandparent>
670 ▾ <Wrapper>
671 ▾ <Parent>
672 <Child key="0">
673 ▾ <Wrapper>
674 ▾ <Parent>
675 <Child key="0">
676 <Child key="1">
677 <Child key="2">
678 ▾ <Wrapper>
679 ▾ <Parent>
680 <Child key="0">
681 <Child key="1">
682 `);
683
684 // Noop (since we're at the root already)
685 utils.act(() =>
686 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
687 );
688 utils.act(() => renderer.update(<Contexts />));
689 expect(state).toMatchInlineSnapshot(`
690 [root]
691 → ▾ <Grandparent>
692 ▾ <Wrapper>
693 ▾ <Parent>
694 <Child key="0">
695 ▾ <Wrapper>
696 ▾ <Parent>
697 <Child key="0">
698 <Child key="1">
699 <Child key="2">
700 ▾ <Wrapper>
701 ▾ <Parent>
702 <Child key="0">
703 <Child key="1">
704 `);
705
706 utils.act(() =>
707 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
708 );
709 utils.act(() => renderer.update(<Contexts />));
710 expect(state).toMatchInlineSnapshot(`
711 [root]
712 ▾ <Grandparent>
713 ▾ <Wrapper>
714 ▾ <Parent>
715 <Child key="0">
716 ▾ <Wrapper>
717 → ▾ <Parent>
718 <Child key="0">
719 <Child key="1">
720 <Child key="2">
721 ▾ <Wrapper>
722 ▾ <Parent>
723 <Child key="0">
724 <Child key="1">
725 `);
726
727 utils.act(() =>
728 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
729 );
730 utils.act(() => renderer.update(<Contexts />));
731 expect(state).toMatchInlineSnapshot(`
732 [root]
733 ▾ <Grandparent>
734 ▾ <Wrapper>
735 ▾ <Parent>
736 <Child key="0">
737 ▾ <Wrapper>
738 ▾ <Parent>
739 <Child key="0">
740 → <Child key="1">
741 <Child key="2">
742 ▾ <Wrapper>
743 ▾ <Parent>
744 <Child key="0">
745 <Child key="1">
746 `);
747
748 // Noop (since we're at the leaf node)
749 utils.act(() =>
750 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
751 );
752 utils.act(() => renderer.update(<Contexts />));
753 expect(state).toMatchInlineSnapshot(`
754 [root]
755 ▾ <Grandparent>
756 ▾ <Wrapper>
757 ▾ <Parent>
758 <Child key="0">
759 ▾ <Wrapper>
760 ▾ <Parent>
761 <Child key="0">
762 → <Child key="1">
763 <Child key="2">
764 ▾ <Wrapper>
765 ▾ <Parent>
766 <Child key="0">
767 <Child key="1">
768 `);
769
770 // Other navigational actions should clear out the temporary owner chain.
771 utils.act(() => dispatch({type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE'}));
772 utils.act(() => renderer.update(<Contexts />));
773 expect(state).toMatchInlineSnapshot(`
774 [root]
775 ▾ <Grandparent>
776 ▾ <Wrapper>
777 ▾ <Parent>
778 <Child key="0">
779 ▾ <Wrapper>
780 ▾ <Parent>
781 → <Child key="0">
782 <Child key="1">
783 <Child key="2">
784 ▾ <Wrapper>
785 ▾ <Parent>
786 <Child key="0">
787 <Child key="1">
788 `);
789
790 // Start a new tree on parent
791 const parentID = ((store.getElementIDAtIndex(5): any): number);
792 utils.act(() =>
793 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: parentID}),
794 );
795 utils.act(() => renderer.update(<Contexts />));
796 expect(state).toMatchInlineSnapshot(`
797 [root]
798 ▾ <Grandparent>
799 ▾ <Wrapper>
800 ▾ <Parent>
801 <Child key="0">
802 ▾ <Wrapper>
803 → ▾ <Parent>
804 <Child key="0">
805 <Child key="1">
806 <Child key="2">
807 ▾ <Wrapper>
808 ▾ <Parent>
809 <Child key="0">
810 <Child key="1">
811 `);
812
813 utils.act(() =>
814 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
815 );
816 utils.act(() => renderer.update(<Contexts />));
817 expect(state).toMatchInlineSnapshot(`
818 [root]
819 → ▾ <Grandparent>
820 ▾ <Wrapper>
821 ▾ <Parent>
822 <Child key="0">
823 ▾ <Wrapper>
824 ▾ <Parent>
825 <Child key="0">
826 <Child key="1">
827 <Child key="2">
828 ▾ <Wrapper>
829 ▾ <Parent>
830 <Child key="0">
831 <Child key="1">
832 `);
833
834 // Noop (since we're at the top)
835 utils.act(() =>
836 dispatch({type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE'}),
837 );
838 utils.act(() => renderer.update(<Contexts />));
839 expect(state).toMatchInlineSnapshot(`
840 [root]
841 → ▾ <Grandparent>
842 ▾ <Wrapper>
843 ▾ <Parent>
844 <Child key="0">
845 ▾ <Wrapper>
846 ▾ <Parent>
847 <Child key="0">
848 <Child key="1">
849 <Child key="2">
850 ▾ <Wrapper>
851 ▾ <Parent>
852 <Child key="0">
853 <Child key="1">
854 `);
855
856 utils.act(() =>
857 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
858 );
859 utils.act(() => renderer.update(<Contexts />));
860 expect(state).toMatchInlineSnapshot(`
861 [root]
862 ▾ <Grandparent>
863 ▾ <Wrapper>
864 ▾ <Parent>
865 <Child key="0">
866 ▾ <Wrapper>
867 → ▾ <Parent>
868 <Child key="0">
869 <Child key="1">
870 <Child key="2">
871 ▾ <Wrapper>
872 ▾ <Parent>
873 <Child key="0">
874 <Child key="1">
875 `);
876
877 // Noop (since we're at the leaf of this owner tree)
878 // It should not be possible to navigate beyond the owner chain leaf.
879 utils.act(() =>
880 dispatch({type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE'}),
881 );
882 utils.act(() => renderer.update(<Contexts />));
883 expect(state).toMatchInlineSnapshot(`
884 [root]
885 ▾ <Grandparent>
886 ▾ <Wrapper>
887 ▾ <Parent>
888 <Child key="0">
889 ▾ <Wrapper>
890 → ▾ <Parent>
891 <Child key="0">
892 <Child key="1">
893 <Child key="2">
894 ▾ <Wrapper>
895 ▾ <Parent>
896 <Child key="0">
897 <Child key="1">
898 `);
899 });
900 });
901
902 describe('search state', () => {
903 it('should find elements matching search text', () => {
904 const Foo = () => null;
905 const Bar = () => null;
906 const Baz = () => null;
907 const Qux = () => null;
908
909 Qux.displayName = `withHOC(${Qux.name})`;
910
911 utils.act(() =>
912 render(
913 <React.Fragment>
914 <Foo />
915 <Bar />
916 <Baz />
917 <Qux />
918 </React.Fragment>,
919 ),
920 );
921
922 let renderer;
923 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
924 expect(state).toMatchInlineSnapshot(`
925 [root]
926 <Foo>
927 <Bar>
928 <Baz>
929 <Qux> [withHOC]
930 `);
931
932 // NOTE: multi-match
933 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
934 utils.act(() => renderer.update(<Contexts />));
935 expect(state).toMatchInlineSnapshot(`
936 [root]
937 <Foo>
938 → <Bar>
939 <Baz>
940 <Qux> [withHOC]
941 `);
942
943 // NOTE: single match
944 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'f'}));
945 utils.act(() => renderer.update(<Contexts />));
946 expect(state).toMatchInlineSnapshot(`
947 [root]
948 → <Foo>
949 <Bar>
950 <Baz>
951 <Qux> [withHOC]
952 `);
953
954 // NOTE: no match
955 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'y'}));
956 utils.act(() => renderer.update(<Contexts />));
957 expect(state).toMatchInlineSnapshot(`
958 [root]
959 → <Foo>
960 <Bar>
961 <Baz>
962 <Qux> [withHOC]
963 `);
964
965 // NOTE: HOC match
966 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'w'}));
967 utils.act(() => renderer.update(<Contexts />));
968 expect(state).toMatchInlineSnapshot(`
969 [root]
970 <Foo>
971 <Bar>
972 <Baz>
973 → <Qux> [withHOC]
974 `);
975 });
976
977 it('should select the next and previous items within the search results', () => {
978 const Foo = () => null;
979 const Bar = () => null;
980 const Baz = () => null;
981
982 utils.act(() =>
983 render(
984 <React.Fragment>
985 <Foo />
986 <Baz />
987 <Bar />
988 <Baz />
989 </React.Fragment>,
990 ),
991 );
992
993 let renderer;
994 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
995 expect(state).toMatchInlineSnapshot(`
996 [root]
997 <Foo>
998 <Baz>
999 <Bar>
1000 <Baz>
1001 `);
1002
1003 // search for "ba"
1004 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1005 utils.act(() => renderer.update(<Contexts />));
1006 expect(state).toMatchInlineSnapshot(`
1007 [root]
1008 <Foo>
1009 → <Baz>
1010 <Bar>
1011 <Baz>
1012 `);
1013
1014 // go to second result
1015 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1016 utils.act(() => renderer.update(<Contexts />));
1017 expect(state).toMatchInlineSnapshot(`
1018 [root]
1019 <Foo>
1020 <Baz>
1021 → <Bar>
1022 <Baz>
1023 `);
1024
1025 // go to third result
1026 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1027 utils.act(() => renderer.update(<Contexts />));
1028 expect(state).toMatchInlineSnapshot(`
1029 [root]
1030 <Foo>
1031 <Baz>
1032 <Bar>
1033 → <Baz>
1034 `);
1035
1036 // go to second result
1037 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1038 utils.act(() => renderer.update(<Contexts />));
1039 expect(state).toMatchInlineSnapshot(`
1040 [root]
1041 <Foo>
1042 <Baz>
1043 → <Bar>
1044 <Baz>
1045 `);
1046
1047 // go to first result
1048 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1049 utils.act(() => renderer.update(<Contexts />));
1050 expect(state).toMatchInlineSnapshot(`
1051 [root]
1052 <Foo>
1053 → <Baz>
1054 <Bar>
1055 <Baz>
1056 `);
1057
1058 // wrap to last result
1059 utils.act(() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}));
1060 utils.act(() => renderer.update(<Contexts />));
1061 expect(state).toMatchInlineSnapshot(`
1062 [root]
1063 <Foo>
1064 <Baz>
1065 <Bar>
1066 → <Baz>
1067 `);
1068
1069 // wrap to first result
1070 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1071 utils.act(() => renderer.update(<Contexts />));
1072 expect(state).toMatchInlineSnapshot(`
1073 [root]
1074 <Foo>
1075 → <Baz>
1076 <Bar>
1077 <Baz>
1078 `);
1079 });
1080
1081 it('should jump directly to a specific search result by index', () => {
1082 const Foo = () => null;
1083 const Bar = () => null;
1084 const Baz = () => null;
1085
1086 utils.act(() =>
1087 render(
1088 <React.Fragment>
1089 <Foo />
1090 <Baz />
1091 <Bar />
1092 <Baz />
1093 </React.Fragment>,
1094 ),
1095 );
1096
1097 let renderer;
1098 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1099
1100 // search for "ba" (matches both <Baz> elements and <Bar>)
1101 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1102 utils.act(() => renderer.update(<Contexts />));
1103 expect(state).toMatchInlineSnapshot(`
1104 [root]
1105 <Foo>
1106 → <Baz>
1107 <Bar>
1108 <Baz>
1109 `);
1110
1111 // jump directly to the third result
1112 utils.act(() => dispatch({type: 'GO_TO_SEARCH_RESULT', payload: 2}));
1113 utils.act(() => renderer.update(<Contexts />));
1114 expect(state).toMatchInlineSnapshot(`
1115 [root]
1116 <Foo>
1117 <Baz>
1118 <Bar>
1119 → <Baz>
1120 `);
1121
1122 // jump directly back to the first result
1123 utils.act(() => dispatch({type: 'GO_TO_SEARCH_RESULT', payload: 0}));
1124 utils.act(() => renderer.update(<Contexts />));
1125 expect(state).toMatchInlineSnapshot(`
1126 [root]
1127 <Foo>
1128 → <Baz>
1129 <Bar>
1130 <Baz>
1131 `);
1132
1133 // out-of-range indices are clamped to the valid range
1134 utils.act(() => dispatch({type: 'GO_TO_SEARCH_RESULT', payload: 99}));
1135 utils.act(() => renderer.update(<Contexts />));
1136 expect(state).toMatchInlineSnapshot(`
1137 [root]
1138 <Foo>
1139 <Baz>
1140 <Bar>
1141 → <Baz>
1142 `);
1143
1144 utils.act(() => dispatch({type: 'GO_TO_SEARCH_RESULT', payload: -5}));
1145 utils.act(() => renderer.update(<Contexts />));
1146 expect(state).toMatchInlineSnapshot(`
1147 [root]
1148 <Foo>
1149 → <Baz>
1150 <Bar>
1151 <Baz>
1152 `);
1153 });
1154
1155 it('should do nothing when jumping to a result with no search matches', () => {
1156 const Foo = () => null;
1157 const Bar = () => null;
1158 const Baz = () => null;
1159
1160 utils.act(() =>
1161 render(
1162 <React.Fragment>
1163 <Foo />
1164 <Baz />
1165 <Bar />
1166 <Baz />
1167 </React.Fragment>,
1168 ),
1169 );
1170
1171 let renderer;
1172 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1173
1174 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'nomatch'}));
1175 utils.act(() => renderer.update(<Contexts />));
1176 expect(state.searchResults).toHaveLength(0);
1177 expect(state.searchIndex).toBe(null);
1178
1179 utils.act(() => dispatch({type: 'GO_TO_SEARCH_RESULT', payload: 0}));
1180 utils.act(() => renderer.update(<Contexts />));
1181 expect(state.searchIndex).toBe(null);
1182 expect(state.inspectedElementID).toBe(null);
1183 expect(state).toMatchInlineSnapshot(`
1184 [root]
1185 <Foo>
1186 <Baz>
1187 <Bar>
1188 <Baz>
1189 `);
1190 });
1191
1192 it('should advance past the selected result when retyping the same search', () => {
1193 const Foo = () => null;
1194 const Bar = () => null;
1195 const Baz = () => null;
1196
1197 utils.act(() =>
1198 render(
1199 <React.Fragment>
1200 <Foo />
1201 <Baz />
1202 <Bar />
1203 <Baz />
1204 </React.Fragment>,
1205 ),
1206 );
1207
1208 let renderer;
1209 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1210
1211 // search for "ba" and step to the second result (<Bar>)
1212 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1213 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1214 utils.act(() => renderer.update(<Contexts />));
1215 expect(state).toMatchInlineSnapshot(`
1216 [root]
1217 <Foo>
1218 <Baz>
1219 → <Bar>
1220 <Baz>
1221 `);
1222
1223 // clear the search; the matched element stays selected
1224 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: ''}));
1225 utils.act(() => renderer.update(<Contexts />));
1226 expect(state).toMatchInlineSnapshot(`
1227 [root]
1228 <Foo>
1229 <Baz>
1230 → <Bar>
1231 <Baz>
1232 `);
1233
1234 // retype the same query: instead of snapping back to the still-selected
1235 // <Bar>, the search advances to the next match (find-next semantics)
1236 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1237 utils.act(() => renderer.update(<Contexts />));
1238 expect(state).toMatchInlineSnapshot(`
1239 [root]
1240 <Foo>
1241 <Baz>
1242 <Bar>
1243 → <Baz>
1244 `);
1245 });
1246
1247 it('should add newly mounted elements to the search results set if they match the current text', async () => {
1248 const Foo = () => null;
1249 const Bar = () => null;
1250 const Baz = () => null;
1251
1252 utils.act(() =>
1253 render(
1254 <React.Fragment>
1255 <Foo />
1256 <Bar />
1257 </React.Fragment>,
1258 ),
1259 );
1260
1261 let renderer;
1262 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1263 expect(state).toMatchInlineSnapshot(`
1264 [root]
1265 <Foo>
1266 <Bar>
1267 `);
1268
1269 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1270 utils.act(() => renderer.update(<Contexts />));
1271 expect(state).toMatchInlineSnapshot(`
1272 [root]
1273 <Foo>
1274 → <Bar>
1275 `);
1276
1277 await utils.actAsync(() =>
1278 render(
1279 <React.Fragment>
1280 <Foo />
1281 <Bar />
1282 <Baz />
1283 </React.Fragment>,
1284 ),
1285 );
1286 utils.act(() => renderer.update(<Contexts />));
1287 expect(state).toMatchInlineSnapshot(`
1288 [root]
1289 <Foo>
1290 → <Bar>
1291 <Baz>
1292 `);
1293
1294 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1295 utils.act(() => renderer.update(<Contexts />));
1296 expect(state).toMatchInlineSnapshot(`
1297 [root]
1298 <Foo>
1299 <Bar>
1300 → <Baz>
1301 `);
1302 });
1303
1304 it('should remove unmounted elements from the search results set', async () => {
1305 const Foo = () => null;
1306 const Bar = () => null;
1307 const Baz = () => null;
1308
1309 utils.act(() =>
1310 render(
1311 <React.Fragment>
1312 <Foo />
1313 <Bar />
1314 <Baz />
1315 </React.Fragment>,
1316 ),
1317 );
1318
1319 let renderer;
1320 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1321 expect(state).toMatchInlineSnapshot(`
1322 [root]
1323 <Foo>
1324 <Bar>
1325 <Baz>
1326 `);
1327
1328 utils.act(() => dispatch({type: 'SET_SEARCH_TEXT', payload: 'ba'}));
1329 utils.act(() => renderer.update(<Contexts />));
1330 expect(state).toMatchInlineSnapshot(`
1331 [root]
1332 <Foo>
1333 → <Bar>
1334 <Baz>
1335 `);
1336
1337 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1338 utils.act(() => renderer.update(<Contexts />));
1339 expect(state).toMatchInlineSnapshot(`
1340 [root]
1341 <Foo>
1342 <Bar>
1343 → <Baz>
1344 `);
1345
1346 await utils.actAsync(() =>
1347 render(
1348 <React.Fragment>
1349 <Foo />
1350 <Bar />
1351 </React.Fragment>,
1352 ),
1353 );
1354 utils.act(() => renderer.update(<Contexts />));
1355 expect(state).toMatchInlineSnapshot(`
1356 [root]
1357 <Foo>
1358 <Bar>
1359 `);
1360
1361 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1362 utils.act(() => renderer.update(<Contexts />));
1363 expect(state).toMatchInlineSnapshot(`
1364 [root]
1365 <Foo>
1366 → <Bar>
1367 `);
1368
1369 // Noop since the list is now one item long
1370 utils.act(() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}));
1371 utils.act(() => renderer.update(<Contexts />));
1372 expect(state).toMatchInlineSnapshot(`
1373 [root]
1374 <Foo>
1375 → <Bar>
1376 `);
1377 });
1378 });
1379
1380 describe('owners state', () => {
1381 it('should support entering and existing the owners tree view', () => {
1382 const Grandparent = () => <Parent />;
1383 const Parent = () => (
1384 <React.Fragment>
1385 <Child />
1386 <Child />
1387 </React.Fragment>
1388 );
1389 const Child = () => null;
1390
1391 utils.act(() => render(<Grandparent />));
1392
1393 let renderer;
1394 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1395 expect(state).toMatchInlineSnapshot(`
1396 [root]
1397 ▾ <Grandparent>
1398 ▾ <Parent>
1399 <Child>
1400 <Child>
1401 `);
1402
1403 const parentID = ((store.getElementIDAtIndex(1): any): number);
1404 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1405 utils.act(() => renderer.update(<Contexts />));
1406 expect(state).toMatchInlineSnapshot(`
1407 [owners]
1408 → ▾ <Parent>
1409 <Child>
1410 <Child>
1411 `);
1412
1413 utils.act(() => dispatch({type: 'RESET_OWNER_STACK'}));
1414 utils.act(() => renderer.update(<Contexts />));
1415 expect(state).toMatchInlineSnapshot(`
1416 [root]
1417 ▾ <Grandparent>
1418 → ▾ <Parent>
1419 <Child>
1420 <Child>
1421 `);
1422 });
1423
1424 it('should remove an element from the owners list if it is unmounted', async () => {
1425 const Grandparent = ({count}) => <Parent count={count} />;
1426 const Parent = ({count}) =>
1427 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1428 const Child = () => null;
1429
1430 utils.act(() => render(<Grandparent count={2} />));
1431
1432 let renderer;
1433 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1434 expect(state).toMatchInlineSnapshot(`
1435 [root]
1436 ▾ <Grandparent>
1437 ▾ <Parent>
1438 <Child key="0">
1439 <Child key="1">
1440 `);
1441
1442 const parentID = ((store.getElementIDAtIndex(1): any): number);
1443 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1444 utils.act(() => renderer.update(<Contexts />));
1445 expect(state).toMatchInlineSnapshot(`
1446 [owners]
1447 → ▾ <Parent>
1448 <Child key="0">
1449 <Child key="1">
1450 `);
1451
1452 await utils.actAsync(() => render(<Grandparent count={1} />));
1453 expect(state).toMatchInlineSnapshot(`
1454 [owners]
1455 → ▾ <Parent>
1456 <Child key="0">
1457 `);
1458
1459 await utils.actAsync(() => render(<Grandparent count={0} />));
1460 expect(state).toMatchInlineSnapshot(`
1461 [owners]
1462 → <Parent>
1463 `);
1464 });
1465
1466 it('should exit the owners list if the current owner is unmounted', async () => {
1467 const Parent = props => props.children || null;
1468 const Child = () => null;
1469
1470 utils.act(() =>
1471 render(
1472 <Parent>
1473 <Child />
1474 </Parent>,
1475 ),
1476 );
1477
1478 let renderer;
1479 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1480 expect(state).toMatchInlineSnapshot(`
1481 [root]
1482 ▾ <Parent>
1483 <Child>
1484 `);
1485
1486 const childID = ((store.getElementIDAtIndex(1): any): number);
1487 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
1488 utils.act(() => renderer.update(<Contexts />));
1489 expect(state).toMatchInlineSnapshot(`
1490 [owners]
1491 → <Child>
1492 `);
1493
1494 await utils.actAsync(() => render(<Parent />));
1495 expect(state).toMatchInlineSnapshot(`
1496 [root]
1497 → <Parent>
1498 `);
1499
1500 const parentID = ((store.getElementIDAtIndex(0): any): number);
1501 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: parentID}));
1502 utils.act(() => renderer.update(<Contexts />));
1503 expect(state).toMatchInlineSnapshot(`
1504 [owners]
1505 → <Parent>
1506 `);
1507
1508 await utils.actAsync(() => unmount());
1509 expect(state).toMatchInlineSnapshot(``);
1510 });
1511
1512 // This tests ensures support for toggling Suspense boundaries outside of the active owners list.
1513 it('should exit the owners list if an element outside the list is selected', () => {
1514 const Grandchild = () => null;
1515 const Child = () => (
1516 <React.Suspense fallback="Loading">
1517 <Grandchild />
1518 </React.Suspense>
1519 );
1520 const Parent = () => (
1521 <React.Suspense fallback="Loading">
1522 <Child />
1523 </React.Suspense>
1524 );
1525
1526 utils.act(() => render(<Parent />));
1527
1528 let renderer;
1529 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
1530 expect(state).toMatchInlineSnapshot(`
1531 [root]
1532 ▾ <Parent>
1533 ▾ <Suspense>
1534 ▾ <Child>
1535 ▾ <Suspense>
1536 <Grandchild>
1537 [suspense-root] rects={null}
1538 <Suspense name="Parent" uniqueSuspenders={false} rects={null}>
1539 <Suspense name="Child" uniqueSuspenders={false} rects={null}>
1540 `);
1541
1542 const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
1543 const childID = ((store.getElementIDAtIndex(2): any): number);
1544 const innerSuspenseID = ((store.getElementIDAtIndex(3): any): number);
1545
1546 utils.act(() => dispatch({type: 'SELECT_OWNER', payload: childID}));
1547 utils.act(() => renderer.update(<Contexts />));
1548 expect(state).toMatchInlineSnapshot(`
1549 [owners]
1550 → ▾ <Child>
1551 ▾ <Suspense>
1552 <Grandchild>
1553 `);
1554
1555 // Toggling a Suspense boundary inside of the flat list should update selected index
1556 utils.act(() =>
1557 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: innerSuspenseID}),
1558 );
1559 utils.act(() => renderer.update(<Contexts />));
1560 expect(state).toMatchInlineSnapshot(`
1561 [owners]
1562 ▾ <Child>
1563 → ▾ <Suspense>
1564 <Grandchild>
1565 `);
1566
1567 // Toggling a Suspense boundary outside of the flat list should exit owners list and update index
1568 utils.act(() =>
1569 dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: outerSuspenseID}),
1570 );
1571 utils.act(() => renderer.update(<Contexts />));
1572 expect(state).toMatchInlineSnapshot(`
1573 [root]
1574 ▾ <Parent>
1575 → ▾ <Suspense>
1576 ▾ <Child>
1577 ▾ <Suspense>
1578 <Grandchild>
1579 [suspense-root] rects={null}
1580 <Suspense name="Parent" uniqueSuspenders={false} rects={null}>
1581 <Suspense name="Child" uniqueSuspenders={false} rects={null}>
1582 `);
1583 });
1584 });
1585
1586 describe('inline errors/warnings state', () => {
1587 const {
1588 clearErrorsAndWarnings: clearErrorsAndWarningsAPI,
1589 clearErrorsForElement: clearErrorsForElementAPI,
1590 clearWarningsForElement: clearWarningsForElementAPI,
1591 } = require('react-devtools-shared/src/backendAPI');
1592
1593 function clearAllErrors() {
1594 utils.act(() => clearErrorsAndWarningsAPI({bridge, store}));
1595 // flush events to the renderer
1596 jest.runAllTimers();
1597 }
1598
1599 function clearErrorsForElement(id) {
1600 const rendererID = store.getRendererIDForElement(id);
1601 utils.act(() => clearErrorsForElementAPI({bridge, id, rendererID}));
1602 // flush events to the renderer
1603 jest.runAllTimers();
1604 }
1605
1606 function clearWarningsForElement(id) {
1607 const rendererID = store.getRendererIDForElement(id);
1608 utils.act(() => clearWarningsForElementAPI({bridge, id, rendererID}));
1609 // flush events to the renderer
1610 jest.runAllTimers();
1611 }
1612
1613 function selectNextErrorOrWarning() {
1614 utils.act(() =>
1615 dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'}),
1616 );
1617 }
1618
1619 function selectPreviousErrorOrWarning() {
1620 utils.act(() =>
1621 dispatch({
1622 type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
1623 }),
1624 );
1625 }
1626
1627 function Child({logError = false, logWarning = false}) {
1628 if (logError === true) {
1629 console.error('test-only: error');
1630 }
1631 if (logWarning === true) {
1632 console.warn('test-only: warning');
1633 }
1634 return null;
1635 }
1636
1637 it('should handle when there are no errors/warnings', () => {
1638 utils.act(() =>
1639 render(
1640 <React.Fragment>
1641 <Child />
1642 <Child />
1643 <Child />
1644 </React.Fragment>,
1645 ),
1646 );
1647
1648 utils.act(() => TestRenderer.create(<Contexts />));
1649
1650 expect(state).toMatchInlineSnapshot(`
1651 [root]
1652 <Child>
1653 <Child>
1654 <Child>
1655 `);
1656
1657 // Next/previous errors should be a no-op
1658 selectPreviousErrorOrWarning();
1659 expect(state).toMatchInlineSnapshot(`
1660 [root]
1661 <Child>
1662 <Child>
1663 <Child>
1664 `);
1665 selectNextErrorOrWarning();
1666 expect(state).toMatchInlineSnapshot(`
1667 [root]
1668 <Child>
1669 <Child>
1670 <Child>
1671 `);
1672
1673 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
1674 expect(state).toMatchInlineSnapshot(`
1675 [root]
1676 → <Child>
1677 <Child>
1678 <Child>
1679 `);
1680
1681 // Next/previous errors should still be a no-op
1682 selectPreviousErrorOrWarning();
1683 expect(state).toMatchInlineSnapshot(`
1684 [root]
1685 → <Child>
1686 <Child>
1687 <Child>
1688 `);
1689 selectNextErrorOrWarning();
1690 expect(state).toMatchInlineSnapshot(`
1691 [root]
1692 → <Child>
1693 <Child>
1694 <Child>
1695 `);
1696 });
1697
1698 it('should cycle through the next errors/warnings and wrap around', () => {
1699 withErrorsOrWarningsIgnored(['test-only:'], () =>
1700 utils.act(() =>
1701 render(
1702 <React.Fragment>
1703 <Child />
1704 <Child logWarning={true} />
1705 <Child />
1706 <Child logError={true} />
1707 <Child />
1708 </React.Fragment>,
1709 ),
1710 ),
1711 );
1712
1713 utils.act(() => TestRenderer.create(<Contexts />));
1714 expect(state).toMatchInlineSnapshot(`
1715 ✕ 1, ⚠ 1
1716 [root]
1717 <Child>
1718 <Child> ⚠
1719 <Child>
1720 <Child> ✕
1721 <Child>
1722 `);
1723
1724 selectNextErrorOrWarning();
1725 expect(state).toMatchInlineSnapshot(`
1726 ✕ 1, ⚠ 1
1727 [root]
1728 <Child>
1729 → <Child> ⚠
1730 <Child>
1731 <Child> ✕
1732 <Child>
1733 `);
1734
1735 selectNextErrorOrWarning();
1736 expect(state).toMatchInlineSnapshot(`
1737 ✕ 1, ⚠ 1
1738 [root]
1739 <Child>
1740 <Child> ⚠
1741 <Child>
1742 → <Child> ✕
1743 <Child>
1744 `);
1745
1746 selectNextErrorOrWarning();
1747 expect(state).toMatchInlineSnapshot(`
1748 ✕ 1, ⚠ 1
1749 [root]
1750 <Child>
1751 → <Child> ⚠
1752 <Child>
1753 <Child> ✕
1754 <Child>
1755 `);
1756 });
1757
1758 it('should cycle through the previous errors/warnings and wrap around', () => {
1759 withErrorsOrWarningsIgnored(['test-only:'], () =>
1760 utils.act(() =>
1761 render(
1762 <React.Fragment>
1763 <Child />
1764 <Child logWarning={true} />
1765 <Child />
1766 <Child logError={true} />
1767 <Child />
1768 </React.Fragment>,
1769 ),
1770 ),
1771 );
1772
1773 utils.act(() => TestRenderer.create(<Contexts />));
1774 expect(state).toMatchInlineSnapshot(`
1775 ✕ 1, ⚠ 1
1776 [root]
1777 <Child>
1778 <Child> ⚠
1779 <Child>
1780 <Child> ✕
1781 <Child>
1782 `);
1783
1784 selectPreviousErrorOrWarning();
1785 expect(state).toMatchInlineSnapshot(`
1786 ✕ 1, ⚠ 1
1787 [root]
1788 <Child>
1789 <Child> ⚠
1790 <Child>
1791 → <Child> ✕
1792 <Child>
1793 `);
1794
1795 selectPreviousErrorOrWarning();
1796 expect(state).toMatchInlineSnapshot(`
1797 ✕ 1, ⚠ 1
1798 [root]
1799 <Child>
1800 → <Child> ⚠
1801 <Child>
1802 <Child> ✕
1803 <Child>
1804 `);
1805
1806 selectPreviousErrorOrWarning();
1807 expect(state).toMatchInlineSnapshot(`
1808 ✕ 1, ⚠ 1
1809 [root]
1810 <Child>
1811 <Child> ⚠
1812 <Child>
1813 → <Child> ✕
1814 <Child>
1815 `);
1816 });
1817
1818 it('should cycle through the next errors/warnings and wrap around with multiple roots', () => {
1819 withErrorsOrWarningsIgnored(['test-only:'], () => {
1820 utils.act(() => {
1821 render(
1822 <React.Fragment>
1823 <Child />
1824 <Child logWarning={true} />,
1825 </React.Fragment>,
1826 );
1827
1828 createContainer();
1829
1830 render(
1831 <React.Fragment>
1832 <Child />
1833 <Child logError={true} />
1834 <Child />
1835 </React.Fragment>,
1836 );
1837 });
1838 });
1839
1840 utils.act(() => TestRenderer.create(<Contexts />));
1841 expect(state).toMatchInlineSnapshot(`
1842 ✕ 1, ⚠ 1
1843 [root]
1844 <Child>
1845 <Child> ⚠
1846 [root]
1847 <Child>
1848 <Child> ✕
1849 <Child>
1850 `);
1851
1852 selectNextErrorOrWarning();
1853 expect(state).toMatchInlineSnapshot(`
1854 ✕ 1, ⚠ 1
1855 [root]
1856 <Child>
1857 → <Child> ⚠
1858 [root]
1859 <Child>
1860 <Child> ✕
1861 <Child>
1862 `);
1863
1864 selectNextErrorOrWarning();
1865 expect(state).toMatchInlineSnapshot(`
1866 ✕ 1, ⚠ 1
1867 [root]
1868 <Child>
1869 <Child> ⚠
1870 [root]
1871 <Child>
1872 → <Child> ✕
1873 <Child>
1874 `);
1875
1876 selectNextErrorOrWarning();
1877 expect(state).toMatchInlineSnapshot(`
1878 ✕ 1, ⚠ 1
1879 [root]
1880 <Child>
1881 → <Child> ⚠
1882 [root]
1883 <Child>
1884 <Child> ✕
1885 <Child>
1886 `);
1887 });
1888
1889 it('should cycle through the previous errors/warnings and wrap around with multiple roots', () => {
1890 withErrorsOrWarningsIgnored(['test-only:'], () => {
1891 utils.act(() => {
1892 render(
1893 <React.Fragment>
1894 <Child />
1895 <Child logWarning={true} />,
1896 </React.Fragment>,
1897 );
1898
1899 createContainer();
1900
1901 render(
1902 <React.Fragment>
1903 <Child />
1904 <Child logError={true} />
1905 <Child />
1906 </React.Fragment>,
1907 );
1908 });
1909 });
1910
1911 utils.act(() => TestRenderer.create(<Contexts />));
1912 expect(state).toMatchInlineSnapshot(`
1913 ✕ 1, ⚠ 1
1914 [root]
1915 <Child>
1916 <Child> ⚠
1917 [root]
1918 <Child>
1919 <Child> ✕
1920 <Child>
1921 `);
1922
1923 selectPreviousErrorOrWarning();
1924 expect(state).toMatchInlineSnapshot(`
1925 ✕ 1, ⚠ 1
1926 [root]
1927 <Child>
1928 <Child> ⚠
1929 [root]
1930 <Child>
1931 → <Child> ✕
1932 <Child>
1933 `);
1934
1935 selectPreviousErrorOrWarning();
1936 expect(state).toMatchInlineSnapshot(`
1937 ✕ 1, ⚠ 1
1938 [root]
1939 <Child>
1940 → <Child> ⚠
1941 [root]
1942 <Child>
1943 <Child> ✕
1944 <Child>
1945 `);
1946
1947 selectPreviousErrorOrWarning();
1948 expect(state).toMatchInlineSnapshot(`
1949 ✕ 1, ⚠ 1
1950 [root]
1951 <Child>
1952 <Child> ⚠
1953 [root]
1954 <Child>
1955 → <Child> ✕
1956 <Child>
1957 `);
1958 });
1959
1960 it('should select the next or previous element relative to the current selection', () => {
1961 withErrorsOrWarningsIgnored(['test-only:'], () =>
1962 utils.act(() =>
1963 render(
1964 <React.Fragment>
1965 <Child />
1966 <Child logWarning={true} />
1967 <Child />
1968 <Child logError={true} />
1969 <Child />
1970 </React.Fragment>,
1971 ),
1972 ),
1973 );
1974
1975 utils.act(() => TestRenderer.create(<Contexts />));
1976 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
1977 expect(state).toMatchInlineSnapshot(`
1978 ✕ 1, ⚠ 1
1979 [root]
1980 <Child>
1981 <Child> ⚠
1982 → <Child>
1983 <Child> ✕
1984 <Child>
1985 `);
1986
1987 selectNextErrorOrWarning();
1988 expect(state).toMatchInlineSnapshot(`
1989 ✕ 1, ⚠ 1
1990 [root]
1991 <Child>
1992 <Child> ⚠
1993 <Child>
1994 → <Child> ✕
1995 <Child>
1996 `);
1997
1998 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 2}));
1999 expect(state).toMatchInlineSnapshot(`
2000 ✕ 1, ⚠ 1
2001 [root]
2002 <Child>
2003 <Child> ⚠
2004 → <Child>
2005 <Child> ✕
2006 <Child>
2007 `);
2008
2009 selectPreviousErrorOrWarning();
2010 expect(state).toMatchInlineSnapshot(`
2011 ✕ 1, ⚠ 1
2012 [root]
2013 <Child>
2014 → <Child> ⚠
2015 <Child>
2016 <Child> ✕
2017 <Child>
2018 `);
2019 });
2020
2021 it('should update correctly when errors/warnings are cleared for a fiber in the list', () => {
2022 withErrorsOrWarningsIgnored(['test-only:'], () =>
2023 utils.act(() =>
2024 render(
2025 <React.Fragment>
2026 <Child logWarning={true} />
2027 <Child logError={true} />
2028 <Child logError={true} />
2029 <Child logWarning={true} />
2030 </React.Fragment>,
2031 ),
2032 ),
2033 );
2034
2035 utils.act(() => TestRenderer.create(<Contexts />));
2036 expect(state).toMatchInlineSnapshot(`
2037 ✕ 2, ⚠ 2
2038 [root]
2039 <Child> ⚠
2040 <Child> ✕
2041 <Child> ✕
2042 <Child> ⚠
2043 `);
2044
2045 // Select the first item in the list
2046 selectNextErrorOrWarning();
2047 expect(state).toMatchInlineSnapshot(`
2048 ✕ 2, ⚠ 2
2049 [root]
2050 → <Child> ⚠
2051 <Child> ✕
2052 <Child> ✕
2053 <Child> ⚠
2054 `);
2055
2056 // Clear warnings (but the next Fiber has only errors)
2057 clearWarningsForElement(store.getElementIDAtIndex(1));
2058 selectNextErrorOrWarning();
2059 expect(state).toMatchInlineSnapshot(`
2060 ✕ 2, ⚠ 2
2061 [root]
2062 <Child> ⚠
2063 → <Child> ✕
2064 <Child> ✕
2065 <Child> ⚠
2066 `);
2067
2068 clearErrorsForElement(store.getElementIDAtIndex(2));
2069
2070 // Should step to the (now) next one in the list.
2071 selectNextErrorOrWarning();
2072 expect(state).toMatchInlineSnapshot(`
2073 ✕ 1, ⚠ 2
2074 [root]
2075 <Child> ⚠
2076 <Child> ✕
2077 <Child>
2078 → <Child> ⚠
2079 `);
2080
2081 // Should skip over the (now) cleared Fiber
2082 selectPreviousErrorOrWarning();
2083 expect(state).toMatchInlineSnapshot(`
2084 ✕ 1, ⚠ 2
2085 [root]
2086 <Child> ⚠
2087 → <Child> ✕
2088 <Child>
2089 <Child> ⚠
2090 `);
2091 });
2092
2093 it('should update correctly when errors/warnings are cleared for the currently selected fiber', () => {
2094 withErrorsOrWarningsIgnored(['test-only:'], () =>
2095 utils.act(() =>
2096 render(
2097 <React.Fragment>
2098 <Child logWarning={true} />
2099 <Child logError={true} />
2100 </React.Fragment>,
2101 ),
2102 ),
2103 );
2104
2105 utils.act(() => TestRenderer.create(<Contexts />));
2106 expect(state).toMatchInlineSnapshot(`
2107 ✕ 1, ⚠ 1
2108 [root]
2109 <Child> ⚠
2110 <Child> ✕
2111 `);
2112
2113 selectNextErrorOrWarning();
2114 expect(state).toMatchInlineSnapshot(`
2115 ✕ 1, ⚠ 1
2116 [root]
2117 → <Child> ⚠
2118 <Child> ✕
2119 `);
2120
2121 clearWarningsForElement(store.getElementIDAtIndex(0));
2122 selectNextErrorOrWarning();
2123 expect(state).toMatchInlineSnapshot(`
2124 ✕ 1, ⚠ 0
2125 [root]
2126 <Child>
2127 → <Child> ✕
2128 `);
2129 });
2130
2131 it('should update correctly when new errors/warnings are added', () => {
2132 withErrorsOrWarningsIgnored(['test-only:'], () =>
2133 utils.act(() =>
2134 render(
2135 <React.Fragment>
2136 <Child logWarning={true} />
2137 <Child />
2138 <Child />
2139 <Child logError={true} />
2140 </React.Fragment>,
2141 ),
2142 ),
2143 );
2144
2145 utils.act(() => TestRenderer.create(<Contexts />));
2146 expect(state).toMatchInlineSnapshot(`
2147 ✕ 1, ⚠ 1
2148 [root]
2149 <Child> ⚠
2150 <Child>
2151 <Child>
2152 <Child> ✕
2153 `);
2154
2155 selectNextErrorOrWarning();
2156 expect(state).toMatchInlineSnapshot(`
2157 ✕ 1, ⚠ 1
2158 [root]
2159 → <Child> ⚠
2160 <Child>
2161 <Child>
2162 <Child> ✕
2163 `);
2164
2165 withErrorsOrWarningsIgnored(['test-only:'], () =>
2166 utils.act(() =>
2167 render(
2168 <React.Fragment>
2169 <Child />
2170 <Child logWarning={true} />
2171 <Child />
2172 <Child />
2173 </React.Fragment>,
2174 ),
2175 ),
2176 );
2177
2178 selectNextErrorOrWarning();
2179 expect(state).toMatchInlineSnapshot(`
2180 ✕ 1, ⚠ 2
2181 [root]
2182 <Child> ⚠
2183 → <Child> ⚠
2184 <Child>
2185 <Child> ✕
2186 `);
2187
2188 selectNextErrorOrWarning();
2189 expect(state).toMatchInlineSnapshot(`
2190 ✕ 1, ⚠ 2
2191 [root]
2192 <Child> ⚠
2193 <Child> ⚠
2194 <Child>
2195 → <Child> ✕
2196 `);
2197
2198 selectNextErrorOrWarning();
2199 expect(state).toMatchInlineSnapshot(`
2200 ✕ 1, ⚠ 2
2201 [root]
2202 → <Child> ⚠
2203 <Child> ⚠
2204 <Child>
2205 <Child> ✕
2206 `);
2207 });
2208
2209 it('should update correctly when all errors/warnings are cleared', () => {
2210 withErrorsOrWarningsIgnored(['test-only:'], () =>
2211 utils.act(() =>
2212 render(
2213 <React.Fragment>
2214 <Child logWarning={true} />
2215 <Child logError={true} />
2216 </React.Fragment>,
2217 ),
2218 ),
2219 );
2220
2221 utils.act(() => TestRenderer.create(<Contexts />));
2222 expect(state).toMatchInlineSnapshot(`
2223 ✕ 1, ⚠ 1
2224 [root]
2225 <Child> ⚠
2226 <Child> ✕
2227 `);
2228
2229 selectNextErrorOrWarning();
2230 expect(state).toMatchInlineSnapshot(`
2231 ✕ 1, ⚠ 1
2232 [root]
2233 → <Child> ⚠
2234 <Child> ✕
2235 `);
2236
2237 clearAllErrors();
2238
2239 selectNextErrorOrWarning();
2240 expect(state).toMatchInlineSnapshot(`
2241 [root]
2242 → <Child>
2243 <Child>
2244 `);
2245
2246 selectPreviousErrorOrWarning();
2247 expect(state).toMatchInlineSnapshot(`
2248 [root]
2249 → <Child>
2250 <Child>
2251 `);
2252 });
2253
2254 it('should update correctly when elements are added/removed', () => {
2255 let errored = false;
2256 function ErrorOnce() {
2257 if (!errored) {
2258 errored = true;
2259 console.error('test-only:one-time-error');
2260 }
2261 return null;
2262 }
2263 withErrorsOrWarningsIgnored(['test-only:'], () =>
2264 utils.act(() =>
2265 render(
2266 <React.Fragment>
2267 <ErrorOnce key="error" />
2268 </React.Fragment>,
2269 ),
2270 ),
2271 );
2272
2273 let renderer;
2274 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
2275 expect(state).toMatchInlineSnapshot(`
2276 ✕ 1, ⚠ 0
2277 [root]
2278 <ErrorOnce key="error"> ✕
2279 `);
2280
2281 withErrorsOrWarningsIgnored(['test-only:'], () =>
2282 utils.act(() =>
2283 render(
2284 <React.Fragment>
2285 <Child />
2286 <ErrorOnce key="error" />
2287 </React.Fragment>,
2288 ),
2289 ),
2290 );
2291
2292 utils.act(() => renderer.update(<Contexts />));
2293 expect(state).toMatchInlineSnapshot(`
2294 ✕ 1, ⚠ 0
2295 [root]
2296 <Child>
2297 <ErrorOnce key="error"> ✕
2298 `);
2299
2300 selectNextErrorOrWarning();
2301 expect(state).toMatchInlineSnapshot(`
2302 ✕ 1, ⚠ 0
2303 [root]
2304 <Child>
2305 → <ErrorOnce key="error"> ✕
2306 `);
2307 });
2308
2309 it('should update correctly when elements are re-ordered', () => {
2310 function ErrorOnce() {
2311 const didErrorRef = React.useRef(false);
2312 if (!didErrorRef.current) {
2313 didErrorRef.current = true;
2314 console.error('test-only:one-time-error');
2315 }
2316 return null;
2317 }
2318 withErrorsOrWarningsIgnored(['test-only:'], () =>
2319 utils.act(() =>
2320 render(
2321 <React.Fragment>
2322 <Child key="A" />
2323 <ErrorOnce key="B" />
2324 <Child key="C" />
2325 <ErrorOnce key="D" />
2326 </React.Fragment>,
2327 ),
2328 ),
2329 );
2330
2331 let renderer;
2332 utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
2333 expect(state).toMatchInlineSnapshot(`
2334 ✕ 2, ⚠ 0
2335 [root]
2336 <Child key="A">
2337 <ErrorOnce key="B"> ✕
2338 <Child key="C">
2339 <ErrorOnce key="D"> ✕
2340 `);
2341
2342 // Select a child
2343 selectNextErrorOrWarning();
2344 utils.act(() => renderer.update(<Contexts />));
2345 expect(state).toMatchInlineSnapshot(`
2346 ✕ 2, ⚠ 0
2347 [root]
2348 <Child key="A">
2349 → <ErrorOnce key="B"> ✕
2350 <Child key="C">
2351 <ErrorOnce key="D"> ✕
2352 `);
2353
2354 // Re-order the tree and ensure indices are updated.
2355 withErrorsOrWarningsIgnored(['test-only:'], () =>
2356 utils.act(() =>
2357 render(
2358 <React.Fragment>
2359 <ErrorOnce key="B" />
2360 <Child key="A" />
2361 <ErrorOnce key="D" />
2362 <Child key="C" />
2363 </React.Fragment>,
2364 ),
2365 ),
2366 );
2367 expect(state).toMatchInlineSnapshot(`
2368 ✕ 2, ⚠ 0
2369 [root]
2370 → <ErrorOnce key="B"> ✕
2371 <Child key="A">
2372 <ErrorOnce key="D"> ✕
2373 <Child key="C">
2374 `);
2375
2376 // Select the next child and ensure the index doesn't break.
2377 selectNextErrorOrWarning();
2378 expect(state).toMatchInlineSnapshot(`
2379 ✕ 2, ⚠ 0
2380 [root]
2381 <ErrorOnce key="B"> ✕
2382 <Child key="A">
2383 → <ErrorOnce key="D"> ✕
2384 <Child key="C">
2385 `);
2386
2387 // Re-order the tree and ensure indices are updated.
2388 withErrorsOrWarningsIgnored(['test-only:'], () =>
2389 utils.act(() =>
2390 render(
2391 <React.Fragment>
2392 <ErrorOnce key="D" />
2393 <ErrorOnce key="B" />
2394 <Child key="A" />
2395 <Child key="C" />
2396 </React.Fragment>,
2397 ),
2398 ),
2399 );
2400 expect(state).toMatchInlineSnapshot(`
2401 ✕ 2, ⚠ 0
2402 [root]
2403 → <ErrorOnce key="D"> ✕
2404 <ErrorOnce key="B"> ✕
2405 <Child key="A">
2406 <Child key="C">
2407 `);
2408 });
2409
2410 it('should update select and auto-expand parts components within hidden parts of the tree', () => {
2411 const Wrapper = ({children}) => children;
2412
2413 store.collapseNodesByDefault = true;
2414
2415 withErrorsOrWarningsIgnored(['test-only:'], () =>
2416 utils.act(() =>
2417 render(
2418 <React.Fragment>
2419 <Wrapper>
2420 <Child logWarning={true} />
2421 </Wrapper>
2422 <Wrapper>
2423 <Wrapper>
2424 <Child logWarning={true} />
2425 </Wrapper>
2426 </Wrapper>
2427 </React.Fragment>,
2428 ),
2429 ),
2430 );
2431
2432 utils.act(() => TestRenderer.create(<Contexts />));
2433 expect(state).toMatchInlineSnapshot(`
2434 ✕ 0, ⚠ 2
2435 [root]
2436 ▸ <Wrapper>
2437 ▸ <Wrapper>
2438 `);
2439
2440 selectNextErrorOrWarning();
2441 expect(state).toMatchInlineSnapshot(`
2442 ✕ 0, ⚠ 2
2443 [root]
2444 ▾ <Wrapper>
2445 → <Child> ⚠
2446 ▸ <Wrapper>
2447 `);
2448
2449 selectNextErrorOrWarning();
2450 expect(state).toMatchInlineSnapshot(`
2451 ✕ 0, ⚠ 2
2452 [root]
2453 ▾ <Wrapper>
2454 <Child> ⚠
2455 ▾ <Wrapper>
2456 ▾ <Wrapper>
2457 → <Child> ⚠
2458 `);
2459 });
2460
2461 it('should preserve errors for fibers even if they are filtered out of the tree initially', () => {
2462 const Wrapper = ({children}) => children;
2463
2464 withErrorsOrWarningsIgnored(['test-only:'], () =>
2465 utils.act(() =>
2466 render(
2467 <React.Fragment>
2468 <Wrapper>
2469 <Child logWarning={true} />
2470 </Wrapper>
2471 <Wrapper>
2472 <Wrapper>
2473 <Child logWarning={true} />
2474 </Wrapper>
2475 </Wrapper>
2476 </React.Fragment>,
2477 ),
2478 ),
2479 );
2480
2481 store.componentFilters = [utils.createDisplayNameFilter('Child')];
2482
2483 utils.act(() => TestRenderer.create(<Contexts />));
2484 expect(state).toMatchInlineSnapshot(`
2485 [root]
2486 <Wrapper>
2487 ▾ <Wrapper>
2488 <Wrapper>
2489 `);
2490
2491 utils.act(() => {
2492 store.componentFilters = [];
2493 });
2494 expect(state).toMatchInlineSnapshot(`
2495 ✕ 0, ⚠ 2
2496 [root]
2497 ▾ <Wrapper>
2498 <Child> ⚠
2499 ▾ <Wrapper>
2500 ▾ <Wrapper>
2501 <Child> ⚠
2502 `);
2503
2504 selectNextErrorOrWarning();
2505 expect(state).toMatchInlineSnapshot(`
2506 ✕ 0, ⚠ 2
2507 [root]
2508 ▾ <Wrapper>
2509 → <Child> ⚠
2510 ▾ <Wrapper>
2511 ▾ <Wrapper>
2512 <Child> ⚠
2513 `);
2514 });
2515
2516 describe('suspense', () => {
2517 // This verifies that we don't flush before the tree has been committed.
2518 it('should properly handle errors/warnings from components inside of delayed Suspense', async () => {
2519 const NeverResolves = React.lazy(() => new Promise(() => {}));
2520
2521 withErrorsOrWarningsIgnored(['test-only:'], () =>
2522 utils.act(() =>
2523 render(
2524 <React.Suspense fallback={null}>
2525 <Child logWarning={true} />
2526 <NeverResolves />
2527 </React.Suspense>,
2528 ),
2529 ),
2530 );
2531 utils.act(() => TestRenderer.create(<Contexts />));
2532
2533 jest.runAllTimers();
2534
2535 expect(state).toMatchInlineSnapshot(`
2536 [root]
2537 <Suspense>
2538 [suspense-root] rects={null}
2539 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2540 `);
2541
2542 selectNextErrorOrWarning();
2543
2544 expect(state).toMatchInlineSnapshot(`
2545 [root]
2546 <Suspense>
2547 [suspense-root] rects={null}
2548 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2549 `);
2550 });
2551
2552 it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
2553 async function fakeImport(result) {
2554 return {default: result};
2555 }
2556 const LazyComponent = React.lazy(() => fakeImport(Child));
2557
2558 withErrorsOrWarningsIgnored(['test-only:'], () =>
2559 utils.act(() =>
2560 render(
2561 <React.Suspense fallback={null}>
2562 <Child logWarning={true} />
2563 <LazyComponent />
2564 </React.Suspense>,
2565 ),
2566 ),
2567 );
2568 utils.act(() => TestRenderer.create(<Contexts />));
2569
2570 expect(state).toMatchInlineSnapshot(`
2571 [root]
2572 <Suspense>
2573 [suspense-root] rects={null}
2574 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2575 `);
2576
2577 await Promise.resolve();
2578 withErrorsOrWarningsIgnored(['test-only:'], () =>
2579 utils.act(() =>
2580 render(
2581 <React.Suspense fallback={null}>
2582 <Child logWarning={true} />
2583 <LazyComponent />
2584 </React.Suspense>,
2585 ),
2586 ),
2587 );
2588
2589 expect(state).toMatchInlineSnapshot(`
2590 ✕ 0, ⚠ 1
2591 [root]
2592 ▾ <Suspense>
2593 <Child> ⚠
2594 <Child>
2595 [suspense-root] rects={null}
2596 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2597 `);
2598 });
2599
2600 it('should properly show errors/warnings from components in the Suspense fallback tree', async () => {
2601 async function fakeImport(result) {
2602 return {default: result};
2603 }
2604 const LazyComponent = React.lazy(() => fakeImport(Child));
2605
2606 const Fallback = () => <Child logError={true} />;
2607
2608 withErrorsOrWarningsIgnored(['test-only:'], () =>
2609 utils.act(() =>
2610 render(
2611 <React.Suspense fallback={<Fallback />}>
2612 <LazyComponent />
2613 </React.Suspense>,
2614 ),
2615 ),
2616 );
2617 utils.act(() => TestRenderer.create(<Contexts />));
2618
2619 expect(state).toMatchInlineSnapshot(`
2620 ✕ 1, ⚠ 0
2621 [root]
2622 ▾ <Suspense>
2623 ▾ <Fallback>
2624 <Child> ✕
2625 [suspense-root] rects={null}
2626 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2627 `);
2628
2629 await Promise.resolve();
2630 withErrorsOrWarningsIgnored(['test-only:'], () =>
2631 utils.act(() =>
2632 render(
2633 <React.Suspense fallback={<Fallback />}>
2634 <LazyComponent />
2635 </React.Suspense>,
2636 ),
2637 ),
2638 );
2639
2640 expect(state).toMatchInlineSnapshot(`
2641 [root]
2642 ▾ <Suspense>
2643 <Child>
2644 [suspense-root] rects={null}
2645 <Suspense name="Unknown" uniqueSuspenders={true} rects={null}>
2646 `);
2647 });
2648 });
2649
2650 describe('error boundaries', () => {
2651 it('should properly handle errors from components that dont mount because of an error', () => {
2652 class ErrorBoundary extends React.Component {
2653 state = {error: null};
2654 static getDerivedStateFromError(error) {
2655 return {error};
2656 }
2657 render() {
2658 if (this.state.error) {
2659 return null;
2660 }
2661 return this.props.children;
2662 }
2663 }
2664
2665 class BadRender extends React.Component {
2666 render() {
2667 console.error('test-only: I am about to throw!');
2668 throw new Error('test-only: Oops!');
2669 }
2670 }
2671
2672 withErrorsOrWarningsIgnored(
2673 ['test-only:', 'React will try to recreate this component tree'],
2674 () => {
2675 utils.act(() =>
2676 render(
2677 <ErrorBoundary>
2678 <BadRender />
2679 </ErrorBoundary>,
2680 ),
2681 );
2682 },
2683 );
2684
2685 utils.act(() => TestRenderer.create(<Contexts />));
2686
2687 expect(store).toMatchInlineSnapshot(`
2688 [root]
2689 <ErrorBoundary>
2690 `);
2691
2692 selectNextErrorOrWarning();
2693 expect(state).toMatchInlineSnapshot(`
2694 [root]
2695 <ErrorBoundary>
2696 `);
2697
2698 utils.act(() => unmount());
2699 expect(state).toMatchInlineSnapshot(``);
2700
2701 // Should be a noop
2702 selectNextErrorOrWarning();
2703 expect(state).toMatchInlineSnapshot(``);
2704 });
2705
2706 it('should properly handle warnings from components that dont mount because of an error', () => {
2707 class ErrorBoundary extends React.Component {
2708 state = {error: null};
2709 static getDerivedStateFromError(error) {
2710 return {error};
2711 }
2712 render() {
2713 if (this.state.error) {
2714 return null;
2715 }
2716 return this.props.children;
2717 }
2718 }
2719
2720 class LogsWarning extends React.Component {
2721 render() {
2722 console.warn('test-only: I am about to throw!');
2723 return <ThrowsError />;
2724 }
2725 }
2726 class ThrowsError extends React.Component {
2727 render() {
2728 throw new Error('test-only: Oops!');
2729 }
2730 }
2731
2732 withErrorsOrWarningsIgnored(
2733 ['test-only:', 'React will try to recreate this component tree'],
2734 () => {
2735 utils.act(() =>
2736 render(
2737 <ErrorBoundary>
2738 <LogsWarning />
2739 </ErrorBoundary>,
2740 ),
2741 );
2742 },
2743 );
2744
2745 utils.act(() => TestRenderer.create(<Contexts />));
2746
2747 expect(store).toMatchInlineSnapshot(`
2748 [root]
2749 <ErrorBoundary>
2750 `);
2751
2752 selectNextErrorOrWarning();
2753 expect(state).toMatchInlineSnapshot(`
2754 [root]
2755 <ErrorBoundary>
2756 `);
2757
2758 utils.act(() => unmount());
2759 expect(state).toMatchInlineSnapshot(``);
2760
2761 // Should be a noop
2762 selectNextErrorOrWarning();
2763 expect(state).toMatchInlineSnapshot(``);
2764 });
2765
2766 it('should properly handle errors/warnings from inside of an error boundary', () => {
2767 class ErrorBoundary extends React.Component {
2768 state = {error: null};
2769 static getDerivedStateFromError(error) {
2770 return {error};
2771 }
2772 render() {
2773 if (this.state.error) {
2774 return <Child logError={true} />;
2775 }
2776 return this.props.children;
2777 }
2778 }
2779
2780 class BadRender extends React.Component {
2781 render() {
2782 console.error('test-only: I am about to throw!');
2783 throw new Error('test-only: Oops!');
2784 }
2785 }
2786
2787 withErrorsOrWarningsIgnored(
2788 ['test-only:', 'React will try to recreate this component tree'],
2789 () => {
2790 utils.act(() =>
2791 render(
2792 <ErrorBoundary>
2793 <BadRender />
2794 </ErrorBoundary>,
2795 ),
2796 );
2797 },
2798 );
2799
2800 utils.act(() => TestRenderer.create(<Contexts />));
2801
2802 expect(store).toMatchInlineSnapshot(`
2803 ✕ 1, ⚠ 0
2804 [root]
2805 ▾ <ErrorBoundary>
2806 <Child> ✕
2807 `);
2808
2809 selectNextErrorOrWarning();
2810 expect(state).toMatchInlineSnapshot(`
2811 ✕ 1, ⚠ 0
2812 [root]
2813 ▾ <ErrorBoundary>
2814 → <Child> ✕
2815 `);
2816 });
2817 });
2818 });
2819 });