main
js 1,337 lines 33.8 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 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactNoop;
15 let Scheduler;
16 let waitForAll;
17 let waitFor;
18 let waitForPaint;
19
20 describe('ReactIncrementalSideEffects', () => {
21 beforeEach(() => {
22 jest.resetModules();
23
24 React = require('react');
25 ReactNoop = require('react-noop-renderer');
26 Scheduler = require('scheduler');
27
28 const InternalTestUtils = require('internal-test-utils');
29 waitForAll = InternalTestUtils.waitForAll;
30 waitFor = InternalTestUtils.waitFor;
31 waitForPaint = InternalTestUtils.waitForPaint;
32 });
33
34 // Note: This is based on a similar component we use in www. We can delete
35 // once the extra div wrapper is no longer necessary.
36 function LegacyHiddenDiv({children, mode}) {
37 return (
38 <div hidden={mode === 'hidden'}>
39 <React.unstable_LegacyHidden
40 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
41 {children}
42 </React.unstable_LegacyHidden>
43 </div>
44 );
45 }
46
47 it('can update child nodes of a host instance', async () => {
48 function Bar(props) {
49 return <span>{props.text}</span>;
50 }
51
52 function Foo(props) {
53 return (
54 <div>
55 <Bar text={props.text} />
56 {props.text === 'World' ? <Bar text={props.text} /> : null}
57 </div>
58 );
59 }
60
61 ReactNoop.render(<Foo text="Hello" />);
62 await waitForAll([]);
63 expect(ReactNoop).toMatchRenderedOutput(
64 <div>
65 <span>Hello</span>
66 </div>,
67 );
68
69 ReactNoop.render(<Foo text="World" />);
70 await waitForAll([]);
71 expect(ReactNoop).toMatchRenderedOutput(
72 <div>
73 <span>World</span>
74 <span>World</span>
75 </div>,
76 );
77 });
78
79 it('can update child nodes of a fragment', async function () {
80 function Bar(props) {
81 return <span>{props.text}</span>;
82 }
83
84 function Foo(props) {
85 return (
86 <div>
87 <Bar text={props.text} />
88 {props.text === 'World'
89 ? [<Bar key="a" text={props.text} />, <div key="b" />]
90 : props.text === 'Hi'
91 ? [<div key="b" />, <Bar key="a" text={props.text} />]
92 : null}
93 <span prop="test" />
94 </div>
95 );
96 }
97
98 ReactNoop.render(<Foo text="Hello" />);
99 await waitForAll([]);
100 expect(ReactNoop).toMatchRenderedOutput(
101 <div>
102 <span>Hello</span>
103 <span prop="test" />
104 </div>,
105 );
106
107 ReactNoop.render(<Foo text="World" />);
108 await waitForAll([]);
109 expect(ReactNoop).toMatchRenderedOutput(
110 <div>
111 <span>World</span>
112 <span>World</span>
113 <div />
114 <span prop="test" />
115 </div>,
116 );
117
118 ReactNoop.render(<Foo text="Hi" />);
119 await waitForAll([]);
120 expect(ReactNoop).toMatchRenderedOutput(
121 <div>
122 <span>Hi</span>
123 <div />
124 <span>Hi</span>
125 <span prop="test" />
126 </div>,
127 );
128 });
129
130 it('can update child nodes rendering into text nodes', async function () {
131 function Bar(props) {
132 return props.text;
133 }
134
135 function Foo(props) {
136 return (
137 <div>
138 <Bar text={props.text} />
139 {props.text === 'World'
140 ? [<Bar key="a" text={props.text} />, '!']
141 : null}
142 </div>
143 );
144 }
145
146 ReactNoop.render(<Foo text="Hello" />);
147 await waitForAll([]);
148 expect(ReactNoop).toMatchRenderedOutput(<div>Hello</div>);
149
150 ReactNoop.render(<Foo text="World" />);
151 await waitForAll([]);
152 expect(ReactNoop).toMatchRenderedOutput(<div>WorldWorld!</div>);
153 });
154
155 it('can deletes children either components, host or text', async function () {
156 function Bar(props) {
157 return <span prop={props.children} />;
158 }
159
160 function Foo(props) {
161 return (
162 <div>
163 {props.show
164 ? [<div key="a" />, <Bar key="b">Hello</Bar>, 'World']
165 : []}
166 </div>
167 );
168 }
169
170 ReactNoop.render(<Foo show={true} />);
171 await waitForAll([]);
172 expect(ReactNoop).toMatchRenderedOutput(
173 <div>
174 <div />
175 <span prop="Hello" />
176 World
177 </div>,
178 );
179
180 ReactNoop.render(<Foo show={false} />);
181 await waitForAll([]);
182 expect(ReactNoop).toMatchRenderedOutput(<div />);
183 });
184
185 it('can delete a child that changes type - implicit keys', async function () {
186 let unmounted = false;
187
188 class ClassComponent extends React.Component {
189 componentWillUnmount() {
190 unmounted = true;
191 }
192 render() {
193 return <span prop="Class" />;
194 }
195 }
196
197 function FunctionComponent(props) {
198 return <span prop="Function" />;
199 }
200
201 function Foo(props) {
202 return (
203 <div>
204 {props.useClass ? (
205 <ClassComponent />
206 ) : props.useFunction ? (
207 <FunctionComponent />
208 ) : props.useText ? (
209 'Text'
210 ) : null}
211 Trail
212 </div>
213 );
214 }
215
216 ReactNoop.render(<Foo useClass={true} />);
217 await waitForAll([]);
218 expect(ReactNoop).toMatchRenderedOutput(
219 <div>
220 <span prop="Class" />
221 Trail
222 </div>,
223 );
224
225 expect(unmounted).toBe(false);
226
227 ReactNoop.render(<Foo useFunction={true} />);
228 await waitForAll([]);
229 expect(ReactNoop).toMatchRenderedOutput(
230 <div>
231 <span prop="Function" />
232 Trail
233 </div>,
234 );
235
236 expect(unmounted).toBe(true);
237
238 ReactNoop.render(<Foo useText={true} />);
239 await waitForAll([]);
240 expect(ReactNoop).toMatchRenderedOutput(<div>TextTrail</div>);
241
242 ReactNoop.render(<Foo />);
243 await waitForAll([]);
244 expect(ReactNoop).toMatchRenderedOutput(<div>Trail</div>);
245 });
246
247 it('can delete a child that changes type - explicit keys', async function () {
248 let unmounted = false;
249
250 class ClassComponent extends React.Component {
251 componentWillUnmount() {
252 unmounted = true;
253 }
254 render() {
255 return <span prop="Class" />;
256 }
257 }
258
259 function FunctionComponent(props) {
260 return <span prop="Function" />;
261 }
262
263 function Foo(props) {
264 return (
265 <div>
266 {props.useClass ? (
267 <ClassComponent key="a" />
268 ) : props.useFunction ? (
269 <FunctionComponent key="a" />
270 ) : null}
271 Trail
272 </div>
273 );
274 }
275
276 ReactNoop.render(<Foo useClass={true} />);
277 await waitForAll([]);
278 expect(ReactNoop).toMatchRenderedOutput(
279 <div>
280 <span prop="Class" />
281 Trail
282 </div>,
283 );
284
285 expect(unmounted).toBe(false);
286
287 ReactNoop.render(<Foo useFunction={true} />);
288 await waitForAll([]);
289 expect(ReactNoop).toMatchRenderedOutput(
290 <div>
291 <span prop="Function" />
292 Trail
293 </div>,
294 );
295
296 expect(unmounted).toBe(true);
297
298 ReactNoop.render(<Foo />);
299 await waitForAll([]);
300 expect(ReactNoop).toMatchRenderedOutput(<div>Trail</div>);
301 });
302
303 it('can delete a child when it unmounts inside a portal', async () => {
304 function Bar(props) {
305 return <span prop={props.children} />;
306 }
307
308 const portalContainer =
309 ReactNoop.getOrCreateRootContainer('portalContainer');
310 function Foo(props) {
311 return ReactNoop.createPortal(
312 props.show ? [<div key="a" />, <Bar key="b">Hello</Bar>, 'World'] : [],
313 portalContainer,
314 null,
315 );
316 }
317
318 ReactNoop.render(
319 <div>
320 <Foo show={true} />
321 </div>,
322 );
323 await waitForAll([]);
324 expect(ReactNoop).toMatchRenderedOutput(<div />);
325 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
326 <>
327 <div />
328 <span prop="Hello" />
329 World
330 </>,
331 );
332
333 ReactNoop.render(
334 <div>
335 <Foo show={false} />
336 </div>,
337 );
338 await waitForAll([]);
339 expect(ReactNoop).toMatchRenderedOutput(<div />);
340 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
341
342 ReactNoop.render(
343 <div>
344 <Foo show={true} />
345 </div>,
346 );
347 await waitForAll([]);
348 expect(ReactNoop).toMatchRenderedOutput(<div />);
349 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
350 <>
351 <div />
352 <span prop="Hello" />
353 World
354 </>,
355 );
356
357 ReactNoop.render(null);
358 await waitForAll([]);
359 expect(ReactNoop).toMatchRenderedOutput(null);
360 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
361
362 ReactNoop.render(<Foo show={false} />);
363 await waitForAll([]);
364 expect(ReactNoop).toMatchRenderedOutput(null);
365 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
366
367 ReactNoop.render(<Foo show={true} />);
368 await waitForAll([]);
369 expect(ReactNoop).toMatchRenderedOutput(null);
370 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
371 <>
372 <div />
373 <span prop="Hello" />
374 World
375 </>,
376 );
377
378 ReactNoop.render(null);
379 await waitForAll([]);
380 expect(ReactNoop).toMatchRenderedOutput(null);
381 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
382 });
383
384 it('can delete a child when it unmounts with a portal', async () => {
385 function Bar(props) {
386 return <span prop={props.children} />;
387 }
388
389 const portalContainer =
390 ReactNoop.getOrCreateRootContainer('portalContainer');
391 function Foo(props) {
392 return ReactNoop.createPortal(
393 [<div key="a" />, <Bar key="b">Hello</Bar>, 'World'],
394 portalContainer,
395 null,
396 );
397 }
398
399 ReactNoop.render(
400 <div>
401 <Foo />
402 </div>,
403 );
404 await waitForAll([]);
405 expect(ReactNoop).toMatchRenderedOutput(<div />);
406 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
407 <>
408 <div />
409 <span prop="Hello" />
410 World
411 </>,
412 );
413
414 ReactNoop.render(null);
415 await waitForAll([]);
416 expect(ReactNoop).toMatchRenderedOutput(null);
417 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
418
419 ReactNoop.render(<Foo />);
420 await waitForAll([]);
421 expect(ReactNoop).toMatchRenderedOutput(null);
422 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(
423 <>
424 <div />
425 <span prop="Hello" />
426 World
427 </>,
428 );
429
430 ReactNoop.render(null);
431 await waitForAll([]);
432 expect(ReactNoop).toMatchRenderedOutput(null);
433 expect(ReactNoop.getChildrenAsJSX('portalContainer')).toEqual(null);
434 });
435
436 it('does not update child nodes if a flush is aborted', async () => {
437 function Bar(props) {
438 Scheduler.log('Bar');
439 return <span prop={props.text} />;
440 }
441
442 function Foo(props) {
443 Scheduler.log('Foo');
444 return (
445 <div>
446 <div>
447 <Bar text={props.text} />
448 {props.text === 'Hello' ? <Bar text={props.text} /> : null}
449 </div>
450 <Bar text="Yo" />
451 </div>
452 );
453 }
454
455 ReactNoop.render(<Foo text="Hello" />);
456 await waitForAll(['Foo', 'Bar', 'Bar', 'Bar']);
457 expect(ReactNoop).toMatchRenderedOutput(
458 <div>
459 <div>
460 <span prop="Hello" />
461 <span prop="Hello" />
462 </div>
463 <span prop="Yo" />
464 </div>,
465 );
466
467 React.startTransition(() => {
468 ReactNoop.render(<Foo text="World" />);
469 });
470
471 // Flush some of the work without committing
472 await waitFor(['Foo', 'Bar']);
473 expect(ReactNoop).toMatchRenderedOutput(
474 <div>
475 <div>
476 <span prop="Hello" />
477 <span prop="Hello" />
478 </div>
479 <span prop="Yo" />
480 </div>,
481 );
482 });
483
484 // @gate enableLegacyHidden
485 it('preserves a previously rendered node when deprioritized', async () => {
486 function Middle(props) {
487 Scheduler.log('Middle');
488 return <span prop={props.children} />;
489 }
490
491 function Foo(props) {
492 Scheduler.log('Foo');
493 return (
494 <div>
495 <LegacyHiddenDiv mode="hidden">
496 <Middle>{props.text}</Middle>
497 </LegacyHiddenDiv>
498 </div>
499 );
500 }
501
502 ReactNoop.render(<Foo text="foo" />);
503 await waitForAll(['Foo', 'Middle']);
504
505 expect(ReactNoop.getChildrenAsJSX()).toEqual(
506 <div>
507 <div hidden={true}>
508 <span prop="foo" />
509 </div>
510 </div>,
511 );
512
513 ReactNoop.render(<Foo text="bar" />, () => Scheduler.log('commit'));
514 await waitFor(['Foo', 'commit']);
515 expect(ReactNoop.getChildrenAsJSX()).toEqual(
516 <div>
517 <div hidden={true}>
518 <span prop="foo" />
519 </div>
520 </div>,
521 );
522
523 await waitForAll(['Middle']);
524 expect(ReactNoop.getChildrenAsJSX()).toEqual(
525 <div>
526 <div hidden={true}>
527 <span prop="bar" />
528 </div>
529 </div>,
530 );
531 });
532
533 // @gate enableLegacyHidden
534 it('can reuse side-effects after being preempted', async () => {
535 function Bar(props) {
536 Scheduler.log('Bar');
537 return <span prop={props.children} />;
538 }
539
540 const middleContent = (
541 <div>
542 <Bar>Hello</Bar>
543 <Bar>World</Bar>
544 </div>
545 );
546
547 function Foo(props) {
548 Scheduler.log('Foo');
549 return (
550 <LegacyHiddenDiv mode="hidden">
551 {props.step === 0 ? (
552 <div>
553 <Bar>Hi</Bar>
554 <Bar>{props.text}</Bar>
555 </div>
556 ) : (
557 middleContent
558 )}
559 </LegacyHiddenDiv>
560 );
561 }
562
563 // Init
564 ReactNoop.render(<Foo text="foo" step={0} />);
565 await waitForAll(['Foo', 'Bar', 'Bar']);
566
567 expect(ReactNoop.getChildrenAsJSX()).toEqual(
568 <div hidden={true}>
569 <div>
570 <span prop="Hi" />
571 <span prop="foo" />
572 </div>
573 </div>,
574 );
575
576 // Make a quick update which will schedule low priority work to
577 // update the middle content.
578 ReactNoop.render(<Foo text="bar" step={1} />, () =>
579 Scheduler.log('commit'),
580 );
581 await waitFor(['Foo', 'commit', 'Bar']);
582
583 // The tree remains unchanged.
584 expect(ReactNoop.getChildrenAsJSX()).toEqual(
585 <div hidden={true}>
586 <div>
587 <span prop="Hi" />
588 <span prop="foo" />
589 </div>
590 </div>,
591 );
592
593 // The first Bar has already completed its update but we'll interrupt it to
594 // render some higher priority work. The middle content will bailout so
595 // it remains untouched which means that it should reuse it next time.
596 ReactNoop.render(<Foo text="foo" step={1} />);
597 await waitForAll(['Foo', 'Bar', 'Bar']);
598
599 // Since we did nothing to the middle subtree during the interruption,
600 // we should be able to reuse the reconciliation work that we already did
601 // without restarting. The side-effects should still be replayed.
602
603 expect(ReactNoop.getChildrenAsJSX()).toEqual(
604 <div hidden={true}>
605 <div>
606 <span prop="Hello" />
607 <span prop="World" />
608 </div>
609 </div>,
610 );
611 });
612
613 // @gate enableLegacyHidden
614 it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', async () => {
615 class Bar extends React.Component {
616 shouldComponentUpdate(nextProps) {
617 return this.props.children !== nextProps.children;
618 }
619 render() {
620 Scheduler.log('Bar');
621 return <span prop={this.props.children} />;
622 }
623 }
624
625 class Content extends React.Component {
626 shouldComponentUpdate(nextProps) {
627 return this.props.step !== nextProps.step;
628 }
629 render() {
630 Scheduler.log('Content');
631 return (
632 <div>
633 <Bar>{this.props.step === 0 ? 'Hi' : 'Hello'}</Bar>
634 <Bar>{this.props.step === 0 ? this.props.text : 'World'}</Bar>
635 </div>
636 );
637 }
638 }
639
640 function Foo(props) {
641 Scheduler.log('Foo');
642 return (
643 <LegacyHiddenDiv mode="hidden">
644 <Content step={props.step} text={props.text} />
645 </LegacyHiddenDiv>
646 );
647 }
648
649 // Init
650 ReactNoop.render(<Foo text="foo" step={0} />);
651 await waitForAll(['Foo', 'Content', 'Bar', 'Bar']);
652
653 expect(ReactNoop.getChildrenAsJSX()).toEqual(
654 <div hidden={true}>
655 <div>
656 <span prop="Hi" />
657 <span prop="foo" />
658 </div>
659 </div>,
660 );
661
662 // Make a quick update which will schedule low priority work to
663 // update the middle content.
664 ReactNoop.render(<Foo text="bar" step={1} />);
665 await waitFor(['Foo', 'Content', 'Bar']);
666
667 // The tree remains unchanged.
668 expect(ReactNoop.getChildrenAsJSX()).toEqual(
669 <div hidden={true}>
670 <div>
671 <span prop="Hi" />
672 <span prop="foo" />
673 </div>
674 </div>,
675 );
676
677 // The first Bar has already completed its update but we'll interrupt it to
678 // render some higher priority work. The middle content will bailout so
679 // it remains untouched which means that it should reuse it next time.
680 ReactNoop.render(<Foo text="foo" step={1} />);
681 await waitForAll(['Foo', 'Content', 'Bar', 'Bar']);
682
683 // Since we did nothing to the middle subtree during the interruption,
684 // we should be able to reuse the reconciliation work that we already did
685 // without restarting. The side-effects should still be replayed.
686
687 expect(ReactNoop.getChildrenAsJSX()).toEqual(
688 <div hidden={true}>
689 <div>
690 <span prop="Hello" />
691 <span prop="World" />
692 </div>
693 </div>,
694 );
695 });
696
697 it('can update a completed tree before it has a chance to commit', async () => {
698 function Foo(props) {
699 Scheduler.log('Foo ' + props.step);
700 return <span prop={props.step} />;
701 }
702 React.startTransition(() => {
703 ReactNoop.render(<Foo step={1} />);
704 });
705 // This should be just enough to complete the tree without committing it
706 await waitFor(['Foo 1']);
707 expect(ReactNoop.getChildrenAsJSX()).toEqual(null);
708 // To confirm, perform one more unit of work. The tree should now
709 // be flushed.
710 await waitForPaint([]);
711 expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
712
713 React.startTransition(() => {
714 ReactNoop.render(<Foo step={2} />);
715 });
716 // This should be just enough to complete the tree without committing it
717 await waitFor(['Foo 2']);
718 expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
719 // This time, before we commit the tree, we update the root component with
720 // new props
721
722 React.startTransition(() => {
723 ReactNoop.render(<Foo step={3} />);
724 });
725 expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={1} />);
726 // Now let's commit. We already had a commit that was pending, which will
727 // render 2.
728 await waitForPaint([]);
729 expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={2} />);
730 // If we flush the rest of the work, we should get another commit that
731 // renders 3. If it renders 2 again, that means an update was dropped.
732 await waitForAll(['Foo 3']);
733 expect(ReactNoop.getChildrenAsJSX()).toEqual(<span prop={3} />);
734 });
735
736 // @gate enableLegacyHidden
737 it('updates a child even though the old props is empty', async () => {
738 function Foo(props) {
739 return (
740 <LegacyHiddenDiv mode="hidden">
741 <span prop={1} />
742 </LegacyHiddenDiv>
743 );
744 }
745
746 ReactNoop.render(<Foo />);
747 await waitForAll([]);
748 expect(ReactNoop.getChildrenAsJSX()).toEqual(
749 <div hidden={true}>
750 <span prop={1} />
751 </div>,
752 );
753 });
754
755 // eslint-disable-next-line jest/no-disabled-tests
756 it.skip('can defer side-effects and resume them later on', async () => {
757 class Bar extends React.Component {
758 shouldComponentUpdate(nextProps) {
759 return this.props.idx !== nextProps.idx;
760 }
761 render() {
762 return <span prop={this.props.idx} />;
763 }
764 }
765 function Foo(props) {
766 return (
767 <div>
768 <span prop={props.tick} />
769 <div hidden={true}>
770 <Bar idx={props.idx} />
771 <Bar idx={props.idx + 1} />
772 </div>
773 </div>
774 );
775 }
776 ReactNoop.render(<Foo tick={0} idx={0} />);
777 ReactNoop.flushDeferredPri(40 + 25);
778 expect(ReactNoop).toMatchRenderedOutput(
779 <div>
780 <span prop={0} />
781 <div />
782 </div>,
783 );
784 ReactNoop.render(<Foo tick={1} idx={0} />);
785 ReactNoop.flushDeferredPri(35 + 25);
786 expect(ReactNoop).toMatchRenderedOutput(
787 <div>
788 <span prop={1} />
789 <div>{/*still not rendered yet*/}</div>
790 </div>,
791 );
792 ReactNoop.flushDeferredPri(30 + 25);
793 expect(ReactNoop).toMatchRenderedOutput(
794 <div>
795 <span prop={1} />
796 <div>
797 {/* Now we had enough time to finish the spans. */}
798 <span prop={0} />
799 <span prop={1} />
800 </div>
801 ,
802 </div>,
803 );
804 const innerSpanA =
805 ReactNoop.dangerouslyGetChildren()[0].children[1].children[1];
806 ReactNoop.render(<Foo tick={2} idx={1} />);
807 ReactNoop.flushDeferredPri(30 + 25);
808 expect(ReactNoop).toMatchRenderedOutput(
809 <div>
810 <span prop={2} />
811 <div>
812 {/* Still same old numbers. */}
813 <span prop={0} />
814 <span prop={1} />
815 </div>
816 </div>,
817 );
818 ReactNoop.render(<Foo tick={3} idx={1} />);
819 await waitForAll([]);
820 expect(ReactNoop).toMatchRenderedOutput(
821 <div>
822 <span prop={3} />
823 <div>
824 {/* New numbers. */}
825 <span prop={1} />
826 <span prop={2} />
827 </div>
828 </div>,
829 );
830
831 const innerSpanB =
832 ReactNoop.dangerouslyGetChildren()[0].children[1].children[1];
833 // This should have been an update to an existing instance, not recreation.
834 // We verify that by ensuring that the child instance was the same as
835 // before.
836 expect(innerSpanA).toBe(innerSpanB);
837 });
838
839 // eslint-disable-next-line jest/no-disabled-tests
840 it.skip('can defer side-effects and reuse them later - complex', async function () {
841 let ops = [];
842
843 class Bar extends React.Component {
844 shouldComponentUpdate(nextProps) {
845 return this.props.idx !== nextProps.idx;
846 }
847 render() {
848 ops.push('Bar');
849 return <span prop={this.props.idx} />;
850 }
851 }
852 class Baz extends React.Component {
853 shouldComponentUpdate(nextProps) {
854 return this.props.idx !== nextProps.idx;
855 }
856 render() {
857 ops.push('Baz');
858 return [
859 <Bar key="a" idx={this.props.idx} />,
860 <Bar key="b" idx={this.props.idx} />,
861 ];
862 }
863 }
864 function Foo(props) {
865 ops.push('Foo');
866 return (
867 <div>
868 <span prop={props.tick} />
869 <div hidden={true}>
870 <Baz idx={props.idx} />
871 <Baz idx={props.idx} />
872 <Baz idx={props.idx} />
873 </div>
874 </div>
875 );
876 }
877 ReactNoop.render(<Foo tick={0} idx={0} />);
878 ReactNoop.flushDeferredPri(65 + 5);
879 expect(ReactNoop).toMatchRenderedOutput(
880 <div>
881 <span prop={0} />
882 {/*the spans are down-prioritized and not rendered yet*/}
883 <div />
884 </div>,
885 );
886
887 expect(ops).toEqual(['Foo', 'Baz', 'Bar']);
888 ops = [];
889
890 ReactNoop.render(<Foo tick={1} idx={0} />);
891 ReactNoop.flushDeferredPri(70);
892 expect(ReactNoop).toMatchRenderedOutput(
893 <div>
894 <span prop={1} />
895 {/*still not rendered yet*/}
896 <div />
897 </div>,
898 );
899
900 expect(ops).toEqual(['Foo']);
901 ops = [];
902
903 await waitForAll([]);
904 expect(ReactNoop).toMatchRenderedOutput([
905 <div>
906 <span prop={1} />,
907 <div>
908 {/* Now we had enough time to finish the spans. */}
909 <span prop={0} />,
910 <span prop={0} />,
911 <span prop={0} />,
912 <span prop={0} />,
913 <span prop={0} />,
914 <span prop={0} />,
915 </div>
916 </div>,
917 ]);
918
919 expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar', 'Bar']);
920 ops = [];
921
922 // Now we're going to update the index but we'll only let it finish half
923 // way through.
924 ReactNoop.render(<Foo tick={2} idx={1} />);
925 ReactNoop.flushDeferredPri(95);
926 expect(ReactNoop).toMatchRenderedOutput(
927 <div>
928 <span prop={2} />,
929 <div>
930 {/* Still same old numbers. */}
931 <span prop={0} />
932 <span prop={0} />
933 <span prop={0} />
934 <span prop={0} />
935 <span prop={0} />
936 <span prop={0} />
937 </div>
938 </div>,
939 );
940
941 // We let it finish half way through. That means we'll have one fully
942 // completed Baz, one half-way completed Baz and one fully incomplete Baz.
943 expect(ops).toEqual(['Foo', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar']);
944 ops = [];
945
946 // We'll update again, without letting the new index update yet. Only half
947 // way through.
948 ReactNoop.render(<Foo tick={3} idx={1} />);
949 ReactNoop.flushDeferredPri(50);
950 expect(ReactNoop).toMatchRenderedOutput(
951 <div>
952 <span prop={3} />
953 <div>
954 {/* Old numbers. */}
955 <span prop={0} />
956 <span prop={0} />
957 <span prop={0} />
958 <span prop={0} />
959 <span prop={0} />
960 <span prop={0} />
961 </div>
962 </div>,
963 );
964
965 expect(ops).toEqual(['Foo']);
966 ops = [];
967
968 // We should now be able to reuse some of the work we've already done
969 // and replay those side-effects.
970 await waitForAll([]);
971 expect(ReactNoop).toMatchRenderedOutput([
972 <div>
973 <span prop={3} />,
974 <div>
975 {/* New numbers. */}
976 <span prop={1} />
977 <span prop={1} />
978 <span prop={1} />
979 <span prop={1} />
980 <span prop={1} />
981 <span prop={1} />
982 </div>
983 </div>,
984 ]);
985
986 expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar']);
987 });
988
989 // @gate enableLegacyHidden
990 it('deprioritizes setStates that happens within a deprioritized tree', async () => {
991 const barInstances = [];
992
993 class Bar extends React.Component {
994 constructor() {
995 super();
996 this.state = {active: false};
997 }
998 activate() {
999 this.setState({active: true});
1000 }
1001 render() {
1002 barInstances.push(this);
1003 Scheduler.log('Bar');
1004 return <span prop={this.state.active ? 'X' : this.props.idx} />;
1005 }
1006 }
1007 function Foo(props) {
1008 Scheduler.log('Foo');
1009 return (
1010 <div>
1011 <span prop={props.tick} />
1012 <LegacyHiddenDiv mode="hidden">
1013 <Bar idx={props.idx} />
1014 <Bar idx={props.idx} />
1015 <Bar idx={props.idx} />
1016 </LegacyHiddenDiv>
1017 </div>
1018 );
1019 }
1020 ReactNoop.render(<Foo tick={0} idx={0} />);
1021 await waitForAll(['Foo', 'Bar', 'Bar', 'Bar']);
1022 expect(ReactNoop.getChildrenAsJSX()).toEqual(
1023 <div>
1024 <span prop={0} />
1025 <div hidden={true}>
1026 <span prop={0} />
1027 <span prop={0} />
1028 <span prop={0} />
1029 </div>
1030 </div>,
1031 );
1032
1033 ReactNoop.render(<Foo tick={1} idx={1} />);
1034 await waitFor(['Foo', 'Bar', 'Bar']);
1035 expect(ReactNoop.getChildrenAsJSX()).toEqual(
1036 <div>
1037 {/* Updated */}
1038 <span prop={1} />
1039 <div hidden={true}>
1040 <span prop={0} />
1041 <span prop={0} />
1042 <span prop={0} />
1043 </div>
1044 </div>,
1045 );
1046
1047 barInstances[0].activate();
1048
1049 // This should not be enough time to render the content of all the hidden
1050 // items. Including the set state since that is deprioritized.
1051 // ReactNoop.flushDeferredPri(35);
1052 await waitFor(['Bar']);
1053 expect(ReactNoop.getChildrenAsJSX()).toEqual(
1054 <div>
1055 {/* Updated */}
1056 <span prop={1} />
1057 <div hidden={true}>
1058 {/* Still not updated */}
1059 <span prop={0} />
1060 <span prop={0} />
1061 <span prop={0} />
1062 </div>
1063 </div>,
1064 );
1065
1066 // However, once we render fully, we will have enough time to finish it all
1067 // at once.
1068 await waitForAll(['Bar', 'Bar']);
1069 expect(ReactNoop.getChildrenAsJSX()).toEqual(
1070 <div>
1071 <span prop={1} />
1072 <div hidden={true}>
1073 {/* Now we had enough time to finish the spans. */}
1074 <span prop="X" />
1075 <span prop={1} />
1076 <span prop={1} />
1077 </div>
1078 </div>,
1079 );
1080 });
1081 // TODO: Test that side-effects are not cut off when a work in progress node
1082 // moves to "current" without flushing due to having lower priority. Does this
1083 // even happen? Maybe a child doesn't get processed because it is lower prio?
1084
1085 it('calls callback after update is flushed', async () => {
1086 let instance;
1087 class Foo extends React.Component {
1088 constructor() {
1089 super();
1090 instance = this;
1091 this.state = {text: 'foo'};
1092 }
1093 render() {
1094 return <span prop={this.state.text} />;
1095 }
1096 }
1097
1098 ReactNoop.render(<Foo />);
1099 await waitForAll([]);
1100 expect(ReactNoop).toMatchRenderedOutput(<span prop="foo" />);
1101 let called = false;
1102 instance.setState({text: 'bar'}, () => {
1103 expect(ReactNoop).toMatchRenderedOutput(<span prop="bar" />);
1104 called = true;
1105 });
1106 await waitForAll([]);
1107 expect(called).toBe(true);
1108 });
1109
1110 it('calls setState callback even if component bails out', async () => {
1111 let instance;
1112 class Foo extends React.Component {
1113 constructor() {
1114 super();
1115 instance = this;
1116 this.state = {text: 'foo'};
1117 }
1118 shouldComponentUpdate(nextProps, nextState) {
1119 return this.state.text !== nextState.text;
1120 }
1121 render() {
1122 return <span prop={this.state.text} />;
1123 }
1124 }
1125
1126 ReactNoop.render(<Foo />);
1127 await waitForAll([]);
1128 expect(ReactNoop).toMatchRenderedOutput(<span prop="foo" />);
1129 let called = false;
1130 instance.setState({}, () => {
1131 called = true;
1132 });
1133 await waitForAll([]);
1134 expect(called).toBe(true);
1135 });
1136
1137 // TODO: Test that callbacks are not lost if an update is preempted.
1138
1139 it('calls componentWillUnmount after a deletion, even if nested', async () => {
1140 const ops = [];
1141
1142 class Bar extends React.Component {
1143 componentWillUnmount() {
1144 ops.push(this.props.name);
1145 }
1146 render() {
1147 return <span />;
1148 }
1149 }
1150
1151 class Wrapper extends React.Component {
1152 componentWillUnmount() {
1153 ops.push('Wrapper');
1154 }
1155 render() {
1156 return <Bar name={this.props.name} />;
1157 }
1158 }
1159
1160 function Foo(props) {
1161 return (
1162 <div>
1163 {props.show
1164 ? [
1165 <Bar key="a" name="A" />,
1166 <Wrapper key="b" name="B" />,
1167 <div key="cd">
1168 <Bar name="C" />
1169 <Wrapper name="D" />,
1170 </div>,
1171 [<Bar key="e" name="E" />, <Bar key="f" name="F" />],
1172 ]
1173 : []}
1174 <div>{props.show ? <Bar key="g" name="G" /> : null}</div>
1175 <Bar name="this should not unmount" />
1176 </div>
1177 );
1178 }
1179
1180 ReactNoop.render(<Foo show={true} />);
1181 await waitForAll([]);
1182 expect(ops).toEqual([]);
1183
1184 ReactNoop.render(<Foo show={false} />);
1185 await waitForAll([]);
1186 expect(ops).toEqual([
1187 'A',
1188 'Wrapper',
1189 'B',
1190 'C',
1191 'Wrapper',
1192 'D',
1193 'E',
1194 'F',
1195 'G',
1196 ]);
1197 });
1198
1199 it('calls componentDidMount/Update after insertion/update', async () => {
1200 let ops = [];
1201
1202 class Bar extends React.Component {
1203 componentDidMount() {
1204 ops.push('mount:' + this.props.name);
1205 }
1206 componentDidUpdate() {
1207 ops.push('update:' + this.props.name);
1208 }
1209 render() {
1210 return <span />;
1211 }
1212 }
1213
1214 class Wrapper extends React.Component {
1215 componentDidMount() {
1216 ops.push('mount:wrapper-' + this.props.name);
1217 }
1218 componentDidUpdate() {
1219 ops.push('update:wrapper-' + this.props.name);
1220 }
1221 render() {
1222 return <Bar name={this.props.name} />;
1223 }
1224 }
1225
1226 function Foo(props) {
1227 return (
1228 <div>
1229 <Bar key="a" name="A" />
1230 <Wrapper key="b" name="B" />
1231 <div key="cd">
1232 <Bar name="C" />
1233 <Wrapper name="D" />
1234 </div>
1235 {[<Bar key="e" name="E" />, <Bar key="f" name="F" />]}
1236 <div>
1237 <Bar key="g" name="G" />
1238 </div>
1239 </div>
1240 );
1241 }
1242
1243 ReactNoop.render(<Foo />);
1244 await waitForAll([]);
1245 expect(ops).toEqual([
1246 'mount:A',
1247 'mount:B',
1248 'mount:wrapper-B',
1249 'mount:C',
1250 'mount:D',
1251 'mount:wrapper-D',
1252 'mount:E',
1253 'mount:F',
1254 'mount:G',
1255 ]);
1256
1257 ops = [];
1258
1259 ReactNoop.render(<Foo />);
1260 await waitForAll([]);
1261 expect(ops).toEqual([
1262 'update:A',
1263 'update:B',
1264 'update:wrapper-B',
1265 'update:C',
1266 'update:D',
1267 'update:wrapper-D',
1268 'update:E',
1269 'update:F',
1270 'update:G',
1271 ]);
1272 });
1273
1274 it('invokes ref callbacks after insertion/update/unmount', async () => {
1275 let classInstance = null;
1276
1277 let ops = [];
1278
1279 class ClassComponent extends React.Component {
1280 render() {
1281 classInstance = this;
1282 return <span />;
1283 }
1284 }
1285
1286 function FunctionComponent(props) {
1287 return <span />;
1288 }
1289
1290 function Foo(props) {
1291 return props.show ? (
1292 <div>
1293 <ClassComponent ref={n => ops.push(n)} />
1294 <FunctionComponent ref={n => ops.push(n)} />
1295 <div ref={n => ops.push(n)} />
1296 </div>
1297 ) : null;
1298 }
1299
1300 ReactNoop.render(<Foo show={true} />);
1301
1302 await waitForAll([]);
1303
1304 expect(ops).toEqual([
1305 classInstance,
1306 // no call for function components
1307 {type: 'div', children: [], prop: undefined, hidden: false},
1308 ]);
1309
1310 ops = [];
1311
1312 // Refs that switch function instances get reinvoked
1313 ReactNoop.render(<Foo show={true} />);
1314 await waitForAll([]);
1315 expect(ops).toEqual([
1316 // detach all refs that switched handlers first.
1317 null,
1318 null,
1319 // reattach as a separate phase
1320 classInstance,
1321 {type: 'div', children: [], prop: undefined, hidden: false},
1322 ]);
1323
1324 ops = [];
1325
1326 ReactNoop.render(<Foo show={false} />);
1327 await waitForAll([]);
1328 expect(ops).toEqual([
1329 // unmount
1330 null,
1331 null,
1332 ]);
1333 });
1334
1335 // TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
1336 // expected way for aborted and resumed render life-cycles.
1337 });