main
js 1,912 lines 51.3 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 */
9
10 'use strict';
11
12 import {createEventTarget} from 'dom-event-testing-library';
13
14 let React;
15 let ReactDOM;
16 let ReactDOMClient;
17 let ReactDOMServer;
18 let ReactFeatureFlags;
19 let Scheduler;
20 let Suspense;
21 let act;
22 let assertLog;
23 let waitForAll;
24 let waitFor;
25 let waitForPaint;
26
27 let IdleEventPriority;
28 let ContinuousEventPriority;
29
30 function dispatchMouseHoverEvent(to, from) {
31 if (!to) {
32 to = null;
33 }
34 if (!from) {
35 from = null;
36 }
37 if (from) {
38 const mouseOutEvent = document.createEvent('MouseEvents');
39 mouseOutEvent.initMouseEvent(
40 'mouseout',
41 true,
42 true,
43 window,
44 0,
45 50,
46 50,
47 50,
48 50,
49 false,
50 false,
51 false,
52 false,
53 0,
54 to,
55 );
56 from.dispatchEvent(mouseOutEvent);
57 }
58 if (to) {
59 const mouseOverEvent = document.createEvent('MouseEvents');
60 mouseOverEvent.initMouseEvent(
61 'mouseover',
62 true,
63 true,
64 window,
65 0,
66 50,
67 50,
68 50,
69 50,
70 false,
71 false,
72 false,
73 false,
74 0,
75 from,
76 );
77 to.dispatchEvent(mouseOverEvent);
78 }
79 }
80
81 function dispatchClickEvent(target) {
82 const mouseOutEvent = document.createEvent('MouseEvents');
83 mouseOutEvent.initMouseEvent(
84 'click',
85 true,
86 true,
87 window,
88 0,
89 50,
90 50,
91 50,
92 50,
93 false,
94 false,
95 false,
96 false,
97 0,
98 target,
99 );
100 return target.dispatchEvent(mouseOutEvent);
101 }
102
103 // TODO: There's currently no React DOM API to opt into Idle priority updates,
104 // and there's no native DOM event that maps to idle priority, so this is a
105 // temporary workaround. Need something like ReactDOM.unstable_IdleUpdates.
106 function TODO_scheduleIdleDOMSchedulerTask(fn) {
107 ReactDOM.unstable_runWithPriority(IdleEventPriority, () => {
108 const prevEvent = window.event;
109 window.event = {type: 'message'};
110 try {
111 fn();
112 } finally {
113 window.event = prevEvent;
114 }
115 });
116 }
117
118 function TODO_scheduleContinuousSchedulerTask(fn) {
119 ReactDOM.unstable_runWithPriority(ContinuousEventPriority, () => {
120 const prevEvent = window.event;
121 window.event = {type: 'message'};
122 try {
123 fn();
124 } finally {
125 window.event = prevEvent;
126 }
127 });
128 }
129
130 describe('ReactDOMServerSelectiveHydration', () => {
131 beforeEach(() => {
132 jest.resetModules();
133
134 ReactFeatureFlags = require('shared/ReactFeatureFlags');
135 ReactFeatureFlags.enableCreateEventHandleAPI = true;
136 React = require('react');
137 ReactDOM = require('react-dom');
138 ReactDOMClient = require('react-dom/client');
139 ReactDOMServer = require('react-dom/server');
140 act = require('internal-test-utils').act;
141 Scheduler = require('scheduler');
142 Suspense = React.Suspense;
143
144 const InternalTestUtils = require('internal-test-utils');
145 assertLog = InternalTestUtils.assertLog;
146 waitForAll = InternalTestUtils.waitForAll;
147 waitFor = InternalTestUtils.waitFor;
148 waitForPaint = InternalTestUtils.waitForPaint;
149
150 IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
151 ContinuousEventPriority =
152 require('react-reconciler/constants').ContinuousEventPriority;
153 });
154
155 it('hydrates the target boundary synchronously during a click', async () => {
156 function Child({text}) {
157 Scheduler.log(text);
158 return (
159 <span
160 onClick={e => {
161 e.preventDefault();
162 Scheduler.log('Clicked ' + text);
163 }}>
164 {text}
165 </span>
166 );
167 }
168
169 function App() {
170 Scheduler.log('App');
171 return (
172 <div>
173 <Suspense fallback="Loading...">
174 <Child text="A" />
175 </Suspense>
176 <Suspense fallback="Loading...">
177 <Child text="B" />
178 </Suspense>
179 </div>
180 );
181 }
182
183 const finalHTML = ReactDOMServer.renderToString(<App />);
184
185 assertLog(['App', 'A', 'B']);
186
187 const container = document.createElement('div');
188 // We need this to be in the document since we'll dispatch events on it.
189 document.body.appendChild(container);
190
191 container.innerHTML = finalHTML;
192
193 const span = container.getElementsByTagName('span')[1];
194
195 ReactDOMClient.hydrateRoot(container, <App />);
196
197 // Nothing has been hydrated so far.
198 assertLog([]);
199
200 // This should synchronously hydrate the root App and the second suspense
201 // boundary.
202 const result = dispatchClickEvent(span);
203
204 // The event should have been canceled because we called preventDefault.
205 expect(result).toBe(false);
206
207 // We rendered App, B and then invoked the event without rendering A.
208 assertLog(['App', 'B', 'Clicked B']);
209
210 // After continuing the scheduler, we finally hydrate A.
211 await waitForAll(['A']);
212
213 document.body.removeChild(container);
214 });
215
216 it('hydrates at higher pri if sync did not work first time', async () => {
217 let suspend = false;
218 let resolve;
219 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
220
221 function Child({text}) {
222 if ((text === 'A' || text === 'D') && suspend) {
223 throw promise;
224 }
225 Scheduler.log(text);
226 return (
227 <span
228 onClick={e => {
229 e.preventDefault();
230 Scheduler.log('Clicked ' + text);
231 }}>
232 {text}
233 </span>
234 );
235 }
236
237 function App() {
238 Scheduler.log('App');
239 return (
240 <div>
241 <Suspense fallback="Loading...">
242 <Child text="A" />
243 </Suspense>
244 <Suspense fallback="Loading...">
245 <Child text="B" />
246 </Suspense>
247 <Suspense fallback="Loading...">
248 <Child text="C" />
249 </Suspense>
250 <Suspense fallback="Loading...">
251 <Child text="D" />
252 </Suspense>
253 </div>
254 );
255 }
256
257 const finalHTML = ReactDOMServer.renderToString(<App />);
258
259 assertLog(['App', 'A', 'B', 'C', 'D']);
260
261 const container = document.createElement('div');
262 // We need this to be in the document since we'll dispatch events on it.
263 document.body.appendChild(container);
264
265 container.innerHTML = finalHTML;
266
267 const spanD = container.getElementsByTagName('span')[3];
268
269 suspend = true;
270
271 // A and D will be suspended. We'll click on D which should take
272 // priority, after we unsuspend.
273 ReactDOMClient.hydrateRoot(container, <App />);
274
275 // Nothing has been hydrated so far.
276 assertLog([]);
277
278 // This click target cannot be hydrated yet because it's suspended.
279 await act(() => {
280 const result = dispatchClickEvent(spanD);
281 expect(result).toBe(true);
282 });
283 assertLog([
284 'App',
285 // Continuing rendering will render B next.
286 'B',
287 'C',
288 ]);
289
290 await act(async () => {
291 suspend = false;
292 resolve();
293 await promise;
294 });
295
296 assertLog(['D', 'A']);
297
298 document.body.removeChild(container);
299 });
300
301 it('hydrates at higher pri for secondary discrete events', async () => {
302 let suspend = false;
303 let resolve;
304 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
305
306 function Child({text}) {
307 if ((text === 'A' || text === 'D') && suspend) {
308 throw promise;
309 }
310 Scheduler.log(text);
311 return (
312 <span
313 onClick={e => {
314 e.preventDefault();
315 Scheduler.log('Clicked ' + text);
316 }}>
317 {text}
318 </span>
319 );
320 }
321
322 function App() {
323 Scheduler.log('App');
324 return (
325 <div>
326 <Suspense fallback="Loading...">
327 <Child text="A" />
328 </Suspense>
329 <Suspense fallback="Loading...">
330 <Child text="B" />
331 </Suspense>
332 <Suspense fallback="Loading...">
333 <Child text="C" />
334 </Suspense>
335 <Suspense fallback="Loading...">
336 <Child text="D" />
337 </Suspense>
338 </div>
339 );
340 }
341
342 const finalHTML = ReactDOMServer.renderToString(<App />);
343
344 assertLog(['App', 'A', 'B', 'C', 'D']);
345
346 const container = document.createElement('div');
347 // We need this to be in the document since we'll dispatch events on it.
348 document.body.appendChild(container);
349
350 container.innerHTML = finalHTML;
351
352 const spanA = container.getElementsByTagName('span')[0];
353 const spanC = container.getElementsByTagName('span')[2];
354 const spanD = container.getElementsByTagName('span')[3];
355
356 suspend = true;
357
358 // A and D will be suspended. We'll click on D which should take
359 // priority, after we unsuspend.
360 ReactDOMClient.hydrateRoot(container, <App />);
361
362 // Nothing has been hydrated so far.
363 assertLog([]);
364
365 // This click target cannot be hydrated yet because the first is Suspended.
366 dispatchClickEvent(spanA);
367 dispatchClickEvent(spanC);
368 dispatchClickEvent(spanD);
369
370 assertLog(['App', 'C', 'Clicked C']);
371
372 await act(async () => {
373 suspend = false;
374 resolve();
375 await promise;
376 });
377
378 assertLog([
379 'A',
380 'D',
381 // B should render last since it wasn't clicked.
382 'B',
383 ]);
384
385 document.body.removeChild(container);
386 });
387
388 // @gate www
389 it('hydrates the target boundary synchronously during a click (createEventHandle)', async () => {
390 const setClick = ReactDOM.unstable_createEventHandle('click');
391 let isServerRendering = true;
392
393 function Child({text}) {
394 const ref = React.useRef(null);
395 Scheduler.log(text);
396 if (!isServerRendering) {
397 React.useLayoutEffect(() => {
398 return setClick(ref.current, () => {
399 Scheduler.log('Clicked ' + text);
400 });
401 });
402 }
403
404 return <span ref={ref}>{text}</span>;
405 }
406
407 function App() {
408 Scheduler.log('App');
409 return (
410 <div>
411 <Suspense fallback="Loading...">
412 <Child text="A" />
413 </Suspense>
414 <Suspense fallback="Loading...">
415 <Child text="B" />
416 </Suspense>
417 </div>
418 );
419 }
420
421 const finalHTML = ReactDOMServer.renderToString(<App />);
422
423 assertLog(['App', 'A', 'B']);
424
425 const container = document.createElement('div');
426 // We need this to be in the document since we'll dispatch events on it.
427 document.body.appendChild(container);
428
429 container.innerHTML = finalHTML;
430
431 isServerRendering = false;
432
433 ReactDOMClient.hydrateRoot(container, <App />);
434
435 // Nothing has been hydrated so far.
436 assertLog([]);
437
438 const span = container.getElementsByTagName('span')[1];
439
440 const target = createEventTarget(span);
441
442 // This should synchronously hydrate the root App and the second suspense
443 // boundary.
444 target.virtualclick();
445
446 // We rendered App, B and then invoked the event without rendering A.
447 assertLog(['App', 'B', 'Clicked B']);
448
449 // After continuing the scheduler, we finally hydrate A.
450 await waitForAll(['A']);
451
452 document.body.removeChild(container);
453 });
454
455 // @gate www
456 it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => {
457 let suspend = false;
458 let isServerRendering = true;
459 let resolve;
460 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
461 const setClick = ReactDOM.unstable_createEventHandle('click');
462
463 function Child({text}) {
464 const ref = React.useRef(null);
465 if ((text === 'A' || text === 'D') && suspend) {
466 throw promise;
467 }
468 Scheduler.log(text);
469
470 if (!isServerRendering) {
471 React.useLayoutEffect(() => {
472 return setClick(ref.current, () => {
473 Scheduler.log('Clicked ' + text);
474 });
475 });
476 }
477
478 return <span ref={ref}>{text}</span>;
479 }
480
481 function App() {
482 Scheduler.log('App');
483 return (
484 <div>
485 <Suspense fallback="Loading...">
486 <Child text="A" />
487 </Suspense>
488 <Suspense fallback="Loading...">
489 <Child text="B" />
490 </Suspense>
491 <Suspense fallback="Loading...">
492 <Child text="C" />
493 </Suspense>
494 <Suspense fallback="Loading...">
495 <Child text="D" />
496 </Suspense>
497 </div>
498 );
499 }
500
501 const finalHTML = ReactDOMServer.renderToString(<App />);
502
503 assertLog(['App', 'A', 'B', 'C', 'D']);
504
505 const container = document.createElement('div');
506 // We need this to be in the document since we'll dispatch events on it.
507 document.body.appendChild(container);
508
509 container.innerHTML = finalHTML;
510
511 const spanD = container.getElementsByTagName('span')[3];
512
513 suspend = true;
514 isServerRendering = false;
515
516 // A and D will be suspended. We'll click on D which should take
517 // priority, after we unsuspend.
518 ReactDOMClient.hydrateRoot(container, <App />);
519
520 // Nothing has been hydrated so far.
521 assertLog([]);
522
523 // Continuing rendering will render B next.
524 await act(() => {
525 const target = createEventTarget(spanD);
526 target.virtualclick();
527 });
528 assertLog(['App', 'B', 'C']);
529
530 // After the click, we should prioritize D and the Click first,
531 // and only after that render A and C.
532 await act(async () => {
533 suspend = false;
534 resolve();
535 await promise;
536 });
537
538 // no replay
539 assertLog(['D', 'A']);
540
541 document.body.removeChild(container);
542 });
543
544 // @gate www
545 it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => {
546 const setClick = ReactDOM.unstable_createEventHandle('click');
547 let suspend = false;
548 let isServerRendering = true;
549 let resolve;
550 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
551
552 function Child({text}) {
553 const ref = React.useRef(null);
554 if ((text === 'A' || text === 'D') && suspend) {
555 throw promise;
556 }
557 Scheduler.log(text);
558
559 if (!isServerRendering) {
560 React.useLayoutEffect(() => {
561 return setClick(ref.current, () => {
562 Scheduler.log('Clicked ' + text);
563 });
564 });
565 }
566 return <span ref={ref}>{text}</span>;
567 }
568
569 function App() {
570 Scheduler.log('App');
571 return (
572 <div>
573 <Suspense fallback="Loading...">
574 <Child text="A" />
575 </Suspense>
576 <Suspense fallback="Loading...">
577 <Child text="B" />
578 </Suspense>
579 <Suspense fallback="Loading...">
580 <Child text="C" />
581 </Suspense>
582 <Suspense fallback="Loading...">
583 <Child text="D" />
584 </Suspense>
585 </div>
586 );
587 }
588
589 const finalHTML = ReactDOMServer.renderToString(<App />);
590
591 assertLog(['App', 'A', 'B', 'C', 'D']);
592
593 const container = document.createElement('div');
594 // We need this to be in the document since we'll dispatch events on it.
595 document.body.appendChild(container);
596
597 container.innerHTML = finalHTML;
598
599 const spanA = container.getElementsByTagName('span')[0];
600 const spanC = container.getElementsByTagName('span')[2];
601 const spanD = container.getElementsByTagName('span')[3];
602
603 suspend = true;
604 isServerRendering = false;
605
606 // A and D will be suspended. We'll click on D which should take
607 // priority, after we unsuspend.
608 ReactDOMClient.hydrateRoot(container, <App />);
609
610 // Nothing has been hydrated so far.
611 assertLog([]);
612
613 // This click target cannot be hydrated yet because the first is Suspended.
614 createEventTarget(spanA).virtualclick();
615 createEventTarget(spanC).virtualclick();
616 createEventTarget(spanD).virtualclick();
617
618 assertLog(['App', 'C', 'Clicked C']);
619
620 await act(async () => {
621 suspend = false;
622 resolve();
623 await promise;
624 });
625
626 assertLog([
627 'A',
628 'D',
629 // B should render last since it wasn't clicked.
630 'B',
631 ]);
632
633 document.body.removeChild(container);
634 });
635
636 it('hydrates the hovered targets as higher priority for continuous events', async () => {
637 let suspend = false;
638 let resolve;
639 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
640 function Child({text}) {
641 if ((text === 'A' || text === 'D') && suspend) {
642 throw promise;
643 }
644 Scheduler.log(text);
645 return (
646 <span
647 onClick={e => {
648 e.preventDefault();
649 Scheduler.log('Clicked ' + text);
650 }}
651 onMouseEnter={e => {
652 e.preventDefault();
653 Scheduler.log('Hover ' + text);
654 }}>
655 {text}
656 </span>
657 );
658 }
659
660 function App() {
661 Scheduler.log('App');
662 return (
663 <div>
664 <Suspense fallback="Loading...">
665 <Child text="A" />
666 </Suspense>
667 <Suspense fallback="Loading...">
668 <Child text="B" />
669 </Suspense>
670 <Suspense fallback="Loading...">
671 <Child text="C" />
672 </Suspense>
673 <Suspense fallback="Loading...">
674 <Child text="D" />
675 </Suspense>
676 </div>
677 );
678 }
679 const finalHTML = ReactDOMServer.renderToString(<App />);
680 assertLog(['App', 'A', 'B', 'C', 'D']);
681 const container = document.createElement('div');
682 // We need this to be in the document since we'll dispatch events on it.
683 document.body.appendChild(container);
684
685 container.innerHTML = finalHTML;
686
687 const spanB = container.getElementsByTagName('span')[1];
688 const spanC = container.getElementsByTagName('span')[2];
689 const spanD = container.getElementsByTagName('span')[3];
690
691 suspend = true;
692
693 // A and D will be suspended. We'll click on D which should take
694 // priority, after we unsuspend.
695 ReactDOMClient.hydrateRoot(container, <App />);
696
697 // Nothing has been hydrated so far.
698 assertLog([]);
699
700 await act(() => {
701 // Click D
702 dispatchMouseHoverEvent(spanD, null);
703 dispatchClickEvent(spanD);
704
705 // Hover over B and then C.
706 dispatchMouseHoverEvent(spanB, spanD);
707 dispatchMouseHoverEvent(spanC, spanB);
708
709 assertLog(['App']);
710
711 suspend = false;
712 resolve();
713 });
714
715 // We should prioritize hydrating D first because we clicked it.
716 // but event isnt replayed
717 assertLog([
718 'D',
719 'B', // Ideally this should be later.
720 'C',
721 'Hover C',
722 'A',
723 ]);
724
725 document.body.removeChild(container);
726 });
727
728 it('replays capture phase for continuous events and respects stopPropagation', async () => {
729 let suspend = false;
730 let resolve;
731 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
732
733 function Child({text}) {
734 if ((text === 'A' || text === 'D') && suspend) {
735 throw promise;
736 }
737 Scheduler.log(text);
738 return (
739 <span
740 id={text}
741 onClickCapture={e => {
742 e.preventDefault();
743 Scheduler.log('Capture Clicked ' + text);
744 }}
745 onClick={e => {
746 e.preventDefault();
747 Scheduler.log('Clicked ' + text);
748 }}
749 onMouseEnter={e => {
750 e.preventDefault();
751 Scheduler.log('Mouse Enter ' + text);
752 }}
753 onMouseOut={e => {
754 e.preventDefault();
755 Scheduler.log('Mouse Out ' + text);
756 }}
757 onMouseOutCapture={e => {
758 e.preventDefault();
759 e.stopPropagation();
760 Scheduler.log('Mouse Out Capture ' + text);
761 }}
762 onMouseOverCapture={e => {
763 e.preventDefault();
764 e.stopPropagation();
765 Scheduler.log('Mouse Over Capture ' + text);
766 }}
767 onMouseOver={e => {
768 e.preventDefault();
769 Scheduler.log('Mouse Over ' + text);
770 }}>
771 <div
772 onMouseOverCapture={e => {
773 e.preventDefault();
774 Scheduler.log('Mouse Over Capture Inner ' + text);
775 }}>
776 {text}
777 </div>
778 </span>
779 );
780 }
781
782 function App() {
783 Scheduler.log('App');
784 return (
785 <div
786 onClickCapture={e => {
787 e.preventDefault();
788 Scheduler.log('Capture Clicked Parent');
789 }}
790 onMouseOverCapture={e => {
791 Scheduler.log('Mouse Over Capture Parent');
792 }}>
793 <Suspense fallback="Loading...">
794 <Child text="A" />
795 </Suspense>
796 <Suspense fallback="Loading...">
797 <Child text="B" />
798 </Suspense>
799 <Suspense fallback="Loading...">
800 <Child text="C" />
801 </Suspense>
802 <Suspense fallback="Loading...">
803 <Child text="D" />
804 </Suspense>
805 </div>
806 );
807 }
808
809 const finalHTML = ReactDOMServer.renderToString(<App />);
810
811 assertLog(['App', 'A', 'B', 'C', 'D']);
812
813 const container = document.createElement('div');
814 // We need this to be in the document since we'll dispatch events on it.
815 document.body.appendChild(container);
816
817 container.innerHTML = finalHTML;
818
819 const spanB = document.getElementById('B').firstChild;
820 const spanC = document.getElementById('C').firstChild;
821 const spanD = document.getElementById('D').firstChild;
822
823 suspend = true;
824
825 // A and D will be suspended. We'll click on D which should take
826 // priority, after we unsuspend.
827 ReactDOMClient.hydrateRoot(container, <App />);
828
829 // Nothing has been hydrated so far.
830 assertLog([]);
831
832 await act(async () => {
833 // Click D
834 dispatchMouseHoverEvent(spanD, null);
835 dispatchClickEvent(spanD);
836 // Hover over B and then C.
837 dispatchMouseHoverEvent(spanB, spanD);
838 dispatchMouseHoverEvent(spanC, spanB);
839
840 assertLog(['App']);
841
842 suspend = false;
843 resolve();
844 });
845
846 // We should prioritize hydrating D first because we clicked it.
847 // but event isnt replayed
848 assertLog([
849 'D',
850 'B', // Ideally this should be later.
851 'C',
852 // Mouse out events aren't replayed
853 // 'Mouse Out Capture B',
854 // 'Mouse Out B',
855 'Mouse Over Capture Parent',
856 'Mouse Over Capture C',
857 // Stop propagation stops these
858 // 'Mouse Over Capture Inner C',
859 // 'Mouse Over C',
860 'A',
861 ]);
862
863 // This test shows existing quirk where stopPropagation on mouseout
864 // prevents mouseEnter from firing
865 dispatchMouseHoverEvent(spanC, spanB);
866 assertLog([
867 'Mouse Out Capture B',
868 // stopPropagation stops these
869 // 'Mouse Out B',
870 // 'Mouse Enter C',
871 'Mouse Over Capture Parent',
872 'Mouse Over Capture C',
873 // Stop propagation stops these
874 // 'Mouse Over Capture Inner C',
875 // 'Mouse Over C',
876 ]);
877
878 document.body.removeChild(container);
879 });
880
881 describe('can handle replaying events as part of multiple instances of React', () => {
882 let resolveInner;
883 let resolveOuter;
884 let innerPromise;
885 let outerPromise;
886 let OuterScheduler;
887 let InnerScheduler;
888 let innerDiv;
889
890 let OuterTestUtils;
891 let InnerTestUtils;
892
893 beforeEach(async () => {
894 document.body.innerHTML = '';
895 jest.resetModules();
896 let OuterReactDOMClient;
897 let InnerReactDOMClient;
898
899 jest.isolateModules(() => {
900 OuterReactDOMClient = require('react-dom/client');
901 OuterScheduler = require('scheduler');
902 OuterTestUtils = require('internal-test-utils');
903 });
904 jest.isolateModules(() => {
905 InnerReactDOMClient = require('react-dom/client');
906 InnerScheduler = require('scheduler');
907 InnerTestUtils = require('internal-test-utils');
908 });
909
910 expect(OuterReactDOMClient).not.toBe(InnerReactDOMClient);
911 expect(OuterScheduler).not.toBe(InnerScheduler);
912
913 const outerContainer = document.createElement('div');
914 const innerContainer = document.createElement('div');
915
916 let suspendOuter = false;
917 outerPromise = new Promise(res => {
918 resolveOuter = () => {
919 suspendOuter = false;
920 res();
921 };
922 });
923
924 function Outer() {
925 if (suspendOuter) {
926 OuterScheduler.log('Suspend Outer');
927 throw outerPromise;
928 }
929 OuterScheduler.log('Outer');
930 const innerRoot = outerContainer.querySelector('#inner-root');
931 return (
932 <div
933 id="inner-root"
934 onMouseEnter={() => {
935 Scheduler.log('Outer Mouse Enter');
936 }}
937 dangerouslySetInnerHTML={{
938 __html: innerRoot ? innerRoot.innerHTML : '',
939 }}
940 />
941 );
942 }
943 const OuterApp = () => {
944 return (
945 <Suspense fallback={<div>Loading</div>}>
946 <Outer />
947 </Suspense>
948 );
949 };
950
951 let suspendInner = false;
952 innerPromise = new Promise(res => {
953 resolveInner = () => {
954 suspendInner = false;
955 res();
956 };
957 });
958 function Inner() {
959 if (suspendInner) {
960 InnerScheduler.log('Suspend Inner');
961 throw innerPromise;
962 }
963 InnerScheduler.log('Inner');
964 return (
965 <div
966 id="inner"
967 onMouseEnter={() => {
968 Scheduler.log('Inner Mouse Enter');
969 }}
970 />
971 );
972 }
973 const InnerApp = () => {
974 return (
975 <Suspense fallback={<div>Loading</div>}>
976 <Inner />
977 </Suspense>
978 );
979 };
980
981 document.body.appendChild(outerContainer);
982 const outerHTML = ReactDOMServer.renderToString(<OuterApp />);
983 outerContainer.innerHTML = outerHTML;
984
985 const innerWrapper = document.querySelector('#inner-root');
986 innerWrapper.appendChild(innerContainer);
987 const innerHTML = ReactDOMServer.renderToString(<InnerApp />);
988 innerContainer.innerHTML = innerHTML;
989
990 OuterTestUtils.assertLog(['Outer']);
991 InnerTestUtils.assertLog(['Inner']);
992
993 suspendOuter = true;
994 suspendInner = true;
995
996 await OuterTestUtils.act(() =>
997 OuterReactDOMClient.hydrateRoot(outerContainer, <OuterApp />),
998 );
999 await InnerTestUtils.act(() =>
1000 InnerReactDOMClient.hydrateRoot(innerContainer, <InnerApp />),
1001 );
1002
1003 OuterTestUtils.assertLog(['Suspend Outer']);
1004 InnerTestUtils.assertLog(['Suspend Inner']);
1005
1006 innerDiv = document.querySelector('#inner');
1007
1008 dispatchClickEvent(innerDiv);
1009
1010 await act(() => {
1011 jest.runAllTimers();
1012 Scheduler.unstable_flushAllWithoutAsserting();
1013 OuterScheduler.unstable_flushAllWithoutAsserting();
1014 InnerScheduler.unstable_flushAllWithoutAsserting();
1015 });
1016
1017 OuterTestUtils.assertLog(['Suspend Outer']);
1018
1019 // InnerApp doesn't see the event because OuterApp calls stopPropagation in
1020 // capture phase since the event is blocked on suspended component
1021 InnerTestUtils.assertLog([]);
1022
1023 assertLog([]);
1024 });
1025 afterEach(async () => {
1026 document.body.innerHTML = '';
1027 });
1028
1029 it('Inner hydrates first then Outer', async () => {
1030 dispatchMouseHoverEvent(innerDiv);
1031
1032 await InnerTestUtils.act(async () => {
1033 await OuterTestUtils.act(() => {
1034 resolveInner();
1035 });
1036 });
1037
1038 OuterTestUtils.assertLog(['Suspend Outer']);
1039 // Inner App renders because it is unblocked
1040 InnerTestUtils.assertLog(['Inner']);
1041 // No event is replayed yet
1042 assertLog([]);
1043
1044 dispatchMouseHoverEvent(innerDiv);
1045 OuterTestUtils.assertLog([]);
1046 InnerTestUtils.assertLog([]);
1047 // No event is replayed yet
1048 assertLog([]);
1049
1050 await InnerTestUtils.act(async () => {
1051 await OuterTestUtils.act(() => {
1052 resolveOuter();
1053
1054 // Nothing happens to inner app yet.
1055 // Its blocked on the outer app replaying the event
1056 InnerTestUtils.assertLog([]);
1057 // Outer hydrates and schedules Replay
1058 OuterTestUtils.waitFor(['Outer']);
1059 // No event is replayed yet
1060 assertLog([]);
1061 });
1062 });
1063
1064 // fire scheduled Replay
1065
1066 // First Inner Mouse Enter fires then Outer Mouse Enter
1067 assertLog(['Inner Mouse Enter', 'Outer Mouse Enter']);
1068 });
1069
1070 it('Outer hydrates first then Inner', async () => {
1071 dispatchMouseHoverEvent(innerDiv);
1072
1073 await act(async () => {
1074 resolveOuter();
1075 await outerPromise;
1076 Scheduler.unstable_flushAllWithoutAsserting();
1077 OuterScheduler.unstable_flushAllWithoutAsserting();
1078 InnerScheduler.unstable_flushAllWithoutAsserting();
1079 });
1080
1081 // Outer resolves and scheduled replay
1082 OuterTestUtils.assertLog(['Outer']);
1083 // Inner App is still blocked
1084 InnerTestUtils.assertLog([]);
1085
1086 // Replay outer event
1087 await act(() => {
1088 Scheduler.unstable_flushAllWithoutAsserting();
1089 OuterScheduler.unstable_flushAllWithoutAsserting();
1090 InnerScheduler.unstable_flushAllWithoutAsserting();
1091 });
1092
1093 // Inner is still blocked so when Outer replays the event in capture phase
1094 // inner ends up caling stopPropagation
1095 assertLog([]);
1096 OuterTestUtils.assertLog([]);
1097 InnerTestUtils.assertLog(['Suspend Inner']);
1098
1099 dispatchMouseHoverEvent(innerDiv);
1100 OuterTestUtils.assertLog([]);
1101 InnerTestUtils.assertLog([]);
1102 assertLog([]);
1103
1104 await act(async () => {
1105 resolveInner();
1106 await innerPromise;
1107 Scheduler.unstable_flushAllWithoutAsserting();
1108 OuterScheduler.unstable_flushAllWithoutAsserting();
1109 InnerScheduler.unstable_flushAllWithoutAsserting();
1110 });
1111
1112 // Inner hydrates
1113 InnerTestUtils.assertLog(['Inner']);
1114 // Outer was hydrated earlier
1115 OuterTestUtils.assertLog([]);
1116
1117 // First Inner Mouse Enter fires then Outer Mouse Enter
1118 assertLog(['Inner Mouse Enter', 'Outer Mouse Enter']);
1119
1120 await act(() => {
1121 Scheduler.unstable_flushAllWithoutAsserting();
1122 OuterScheduler.unstable_flushAllWithoutAsserting();
1123 InnerScheduler.unstable_flushAllWithoutAsserting();
1124 });
1125
1126 assertLog([]);
1127 });
1128 });
1129
1130 it('replays event with null target when tree is dismounted', async () => {
1131 let suspend = false;
1132 let resolve;
1133 const promise = new Promise(resolvePromise => {
1134 resolve = () => {
1135 suspend = false;
1136 resolvePromise();
1137 };
1138 });
1139
1140 function Child() {
1141 if (suspend) {
1142 throw promise;
1143 }
1144 Scheduler.log('Child');
1145 return (
1146 <div
1147 onMouseOver={() => {
1148 Scheduler.log('on mouse over');
1149 }}>
1150 Child
1151 </div>
1152 );
1153 }
1154
1155 function App() {
1156 return (
1157 <Suspense>
1158 <Child />
1159 </Suspense>
1160 );
1161 }
1162
1163 const finalHTML = ReactDOMServer.renderToString(<App />);
1164 assertLog(['Child']);
1165
1166 const container = document.createElement('div');
1167
1168 document.body.appendChild(container);
1169 container.innerHTML = finalHTML;
1170 suspend = true;
1171
1172 ReactDOMClient.hydrateRoot(container, <App />);
1173
1174 const childDiv = container.firstElementChild;
1175
1176 await act(async () => {
1177 dispatchMouseHoverEvent(childDiv);
1178
1179 // Not hydrated so event is saved for replay and stopPropagation is called
1180 assertLog([]);
1181
1182 resolve();
1183 await waitFor(['Child']);
1184
1185 ReactDOM.flushSync(() => {
1186 container.removeChild(childDiv);
1187
1188 const container2 = document.createElement('div');
1189 container2.addEventListener('mouseover', () => {
1190 Scheduler.log('container2 mouse over');
1191 });
1192 container2.appendChild(childDiv);
1193 });
1194 });
1195
1196 // Even though the tree is remove the event is still dispatched with native event handler
1197 // on the container firing.
1198 assertLog(['container2 mouse over']);
1199
1200 document.body.removeChild(container);
1201 });
1202
1203 it('hydrates the last target path first for continuous events', async () => {
1204 let suspend = false;
1205 let resolve;
1206 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
1207
1208 function Child({text}) {
1209 if ((text === 'A' || text === 'D') && suspend) {
1210 throw promise;
1211 }
1212 Scheduler.log(text);
1213 return (
1214 <span
1215 onMouseEnter={e => {
1216 e.preventDefault();
1217 Scheduler.log('Hover ' + text);
1218 }}>
1219 {text}
1220 </span>
1221 );
1222 }
1223
1224 function App() {
1225 Scheduler.log('App');
1226 return (
1227 <div>
1228 <Suspense fallback="Loading...">
1229 <Child text="A" />
1230 </Suspense>
1231 <Suspense fallback="Loading...">
1232 <div>
1233 <Suspense fallback="Loading...">
1234 <Child text="B" />
1235 </Suspense>
1236 </div>
1237 <Child text="C" />
1238 </Suspense>
1239 <Suspense fallback="Loading...">
1240 <Child text="D" />
1241 </Suspense>
1242 </div>
1243 );
1244 }
1245
1246 const finalHTML = ReactDOMServer.renderToString(<App />);
1247
1248 assertLog(['App', 'A', 'B', 'C', 'D']);
1249
1250 const container = document.createElement('div');
1251 // We need this to be in the document since we'll dispatch events on it.
1252 document.body.appendChild(container);
1253
1254 container.innerHTML = finalHTML;
1255
1256 const spanB = container.getElementsByTagName('span')[1];
1257 const spanC = container.getElementsByTagName('span')[2];
1258 const spanD = container.getElementsByTagName('span')[3];
1259
1260 suspend = true;
1261
1262 // A and D will be suspended. We'll click on D which should take
1263 // priority, after we unsuspend.
1264 ReactDOMClient.hydrateRoot(container, <App />);
1265
1266 // Nothing has been hydrated so far.
1267 assertLog([]);
1268
1269 // Hover over B and then C.
1270 dispatchMouseHoverEvent(spanB, spanD);
1271 dispatchMouseHoverEvent(spanC, spanB);
1272
1273 await act(async () => {
1274 suspend = false;
1275 resolve();
1276 await promise;
1277 });
1278
1279 // We should prioritize hydrating D first because we clicked it.
1280 // Next we should hydrate C since that's the current hover target.
1281 // Next it doesn't matter if we hydrate A or B first but as an
1282 // implementation detail we're currently hydrating B first since
1283 // we at one point hovered over it and we never deprioritized it.
1284 assertLog(['App', 'C', 'Hover C', 'A', 'B', 'D']);
1285
1286 document.body.removeChild(container);
1287 });
1288
1289 it('hydrates the last explicitly hydrated target at higher priority', async () => {
1290 function Child({text}) {
1291 Scheduler.log(text);
1292 return <span>{text}</span>;
1293 }
1294
1295 function App() {
1296 Scheduler.log('App');
1297 return (
1298 <div>
1299 <Suspense fallback="Loading...">
1300 <Child text="A" />
1301 </Suspense>
1302 <Suspense fallback="Loading...">
1303 <Child text="B" />
1304 </Suspense>
1305 <Suspense fallback="Loading...">
1306 <Child text="C" />
1307 </Suspense>
1308 </div>
1309 );
1310 }
1311
1312 const finalHTML = ReactDOMServer.renderToString(<App />);
1313
1314 assertLog(['App', 'A', 'B', 'C']);
1315
1316 const container = document.createElement('div');
1317 container.innerHTML = finalHTML;
1318
1319 const spanB = container.getElementsByTagName('span')[1];
1320 const spanC = container.getElementsByTagName('span')[2];
1321
1322 const root = ReactDOMClient.hydrateRoot(container, <App />);
1323
1324 // Nothing has been hydrated so far.
1325 assertLog([]);
1326
1327 // Increase priority of B and then C.
1328 root.unstable_scheduleHydration(spanB);
1329 root.unstable_scheduleHydration(spanC);
1330
1331 // We should prioritize hydrating C first because the last added
1332 // gets highest priority followed by the next added.
1333 await waitForAll(['App', 'C', 'B', 'A']);
1334 });
1335
1336 // @gate www
1337 it('hydrates before an update even if hydration moves away from it', async () => {
1338 function Child({text}) {
1339 Scheduler.log(text);
1340 return <span>{text}</span>;
1341 }
1342 const ChildWithBoundary = React.memo(function ({text}) {
1343 return (
1344 <Suspense fallback="Loading...">
1345 <Child text={text} />
1346 <Child text={text.toLowerCase()} />
1347 </Suspense>
1348 );
1349 });
1350
1351 function App({a}) {
1352 Scheduler.log('App');
1353 React.useEffect(() => {
1354 Scheduler.log('Commit');
1355 });
1356 return (
1357 <div>
1358 <ChildWithBoundary text={a} />
1359 <ChildWithBoundary text="B" />
1360 <ChildWithBoundary text="C" />
1361 </div>
1362 );
1363 }
1364
1365 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1366
1367 assertLog(['App', 'A', 'a', 'B', 'b', 'C', 'c']);
1368
1369 const container = document.createElement('div');
1370 container.innerHTML = finalHTML;
1371
1372 // We need this to be in the document since we'll dispatch events on it.
1373 document.body.appendChild(container);
1374
1375 const spanA = container.getElementsByTagName('span')[0];
1376 const spanB = container.getElementsByTagName('span')[2];
1377 const spanC = container.getElementsByTagName('span')[4];
1378
1379 await act(async () => {
1380 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1381 // Hydrate the shell.
1382 await waitFor(['App', 'Commit']);
1383
1384 // Render an update at Idle priority that needs to update A.
1385
1386 TODO_scheduleIdleDOMSchedulerTask(() => {
1387 root.render(<App a="AA" />);
1388 });
1389
1390 // Start rendering. This will force the first boundary to hydrate
1391 // by scheduling it at one higher pri than Idle.
1392 await waitFor([
1393 'App',
1394
1395 // Start hydrating A
1396 'A',
1397 ]);
1398
1399 // Hover over A which (could) schedule at one higher pri than Idle.
1400 dispatchMouseHoverEvent(spanA, null);
1401
1402 // Before, we're done we now switch to hover over B.
1403 // This is meant to test that this doesn't cause us to forget that
1404 // we still have to hydrate A. The first boundary.
1405 // This also tests that we don't do the -1 down-prioritization of
1406 // continuous hover events because that would decrease its priority
1407 // to Idle.
1408 dispatchMouseHoverEvent(spanB, spanA);
1409
1410 // Also click C to prioritize that even higher which resets the
1411 // priority levels.
1412 dispatchClickEvent(spanC);
1413
1414 assertLog([
1415 // Hydrate C first since we clicked it.
1416 'C',
1417 'c',
1418 ]);
1419
1420 await waitForAll([
1421 // Finish hydration of A since we forced it to hydrate.
1422 'A',
1423 'a',
1424 // Also, hydrate B since we hovered over it.
1425 // It's not important which one comes first. A or B.
1426 // As long as they both happen before the Idle update.
1427 'B',
1428 'b',
1429 // Begin the Idle update again.
1430 'App',
1431 'AA',
1432 'aa',
1433 'Commit',
1434 ]);
1435 });
1436
1437 const spanA2 = container.getElementsByTagName('span')[0];
1438 // This is supposed to have been hydrated, not replaced.
1439 expect(spanA).toBe(spanA2);
1440
1441 document.body.removeChild(container);
1442 });
1443
1444 it('fires capture event handlers and native events if content is hydratable during discrete event', async () => {
1445 spyOnDev(console, 'error');
1446 function Child({text}) {
1447 Scheduler.log(text);
1448 const ref = React.useRef();
1449 React.useLayoutEffect(() => {
1450 if (!ref.current) {
1451 return;
1452 }
1453 ref.current.onclick = () => {
1454 Scheduler.log('Native Click ' + text);
1455 };
1456 }, [text]);
1457 return (
1458 <span
1459 ref={ref}
1460 onClickCapture={() => {
1461 Scheduler.log('Capture Clicked ' + text);
1462 }}
1463 onClick={e => {
1464 Scheduler.log('Clicked ' + text);
1465 }}>
1466 {text}
1467 </span>
1468 );
1469 }
1470
1471 function App() {
1472 Scheduler.log('App');
1473 return (
1474 <div>
1475 <Suspense fallback="Loading...">
1476 <Child text="A" />
1477 </Suspense>
1478 <Suspense fallback="Loading...">
1479 <Child text="B" />
1480 </Suspense>
1481 </div>
1482 );
1483 }
1484
1485 const finalHTML = ReactDOMServer.renderToString(<App />);
1486
1487 assertLog(['App', 'A', 'B']);
1488
1489 const container = document.createElement('div');
1490 // We need this to be in the document since we'll dispatch events on it.
1491 document.body.appendChild(container);
1492
1493 container.innerHTML = finalHTML;
1494
1495 const span = container.getElementsByTagName('span')[1];
1496
1497 ReactDOMClient.hydrateRoot(container, <App />);
1498
1499 // Nothing has been hydrated so far.
1500 assertLog([]);
1501
1502 // This should synchronously hydrate the root App and the second suspense
1503 // boundary.
1504 dispatchClickEvent(span);
1505
1506 // We rendered App, B and then invoked the event without rendering A.
1507 assertLog(['App', 'B', 'Capture Clicked B', 'Native Click B', 'Clicked B']);
1508
1509 // After continuing the scheduler, we finally hydrate A.
1510 await waitForAll(['A']);
1511
1512 document.body.removeChild(container);
1513 });
1514
1515 it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
1516 let triggeredParent = false;
1517 let triggeredChild = false;
1518 let suspend = false;
1519 const promise = new Promise(() => {});
1520 function Child() {
1521 if (suspend) {
1522 throw promise;
1523 }
1524 Scheduler.log('Child');
1525 return (
1526 <span
1527 onClickCapture={e => {
1528 e.stopPropagation();
1529 triggeredChild = true;
1530 }}>
1531 Click me
1532 </span>
1533 );
1534 }
1535 function App() {
1536 const onClick = () => {
1537 triggeredParent = true;
1538 };
1539 Scheduler.log('App');
1540 return (
1541 <div
1542 ref={n => {
1543 if (n) n.onclick = onClick;
1544 }}
1545 onClick={onClick}>
1546 <Suspense fallback={null}>
1547 <Child />
1548 </Suspense>
1549 </div>
1550 );
1551 }
1552 const finalHTML = ReactDOMServer.renderToString(<App />);
1553
1554 assertLog(['App', 'Child']);
1555
1556 const container = document.createElement('div');
1557 document.body.appendChild(container);
1558 container.innerHTML = finalHTML;
1559
1560 suspend = true;
1561
1562 ReactDOMClient.hydrateRoot(container, <App />);
1563 // Nothing has been hydrated so far.
1564 assertLog([]);
1565
1566 const span = container.getElementsByTagName('span')[0];
1567 dispatchClickEvent(span);
1568
1569 assertLog(['App']);
1570
1571 dispatchClickEvent(span);
1572
1573 expect(triggeredParent).toBe(false);
1574 expect(triggeredChild).toBe(false);
1575 });
1576
1577 it('can attempt sync hydration if suspended root is still concurrently rendering', async () => {
1578 let suspend = false;
1579 let resolve;
1580 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
1581 function Child({text}) {
1582 if (suspend) {
1583 throw promise;
1584 }
1585 Scheduler.log(text);
1586 return (
1587 <span
1588 onClick={e => {
1589 e.preventDefault();
1590 Scheduler.log('Clicked ' + text);
1591 }}>
1592 {text}
1593 </span>
1594 );
1595 }
1596
1597 function App() {
1598 Scheduler.log('App');
1599 return (
1600 <div>
1601 <Child text="A" />
1602 </div>
1603 );
1604 }
1605
1606 const finalHTML = ReactDOMServer.renderToString(<App />);
1607
1608 assertLog(['App', 'A']);
1609
1610 const container = document.createElement('div');
1611 // We need this to be in the document since we'll dispatch events on it.
1612 document.body.appendChild(container);
1613
1614 container.innerHTML = finalHTML;
1615
1616 const span = container.getElementsByTagName('span')[0];
1617
1618 // We suspend on the client.
1619 suspend = true;
1620
1621 React.startTransition(() => {
1622 ReactDOMClient.hydrateRoot(container, <App />);
1623 });
1624 await waitFor(['App']);
1625
1626 // This should attempt to synchronously hydrate the root, then pause
1627 // because it still suspended
1628 const result = dispatchClickEvent(span);
1629 assertLog(['App']);
1630 // The event should not have been cancelled because we didn't hydrate.
1631 expect(result).toBe(true);
1632
1633 // Finish loading the data
1634 await act(async () => {
1635 suspend = false;
1636 await resolve();
1637 });
1638
1639 // The app should have successfully hydrated and rendered
1640 assertLog(['App', 'A']);
1641
1642 document.body.removeChild(container);
1643 });
1644
1645 it('can force hydration in response to sync update', async () => {
1646 function Child({text}) {
1647 Scheduler.log(`Child ${text}`);
1648 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1649 }
1650 function App({text}) {
1651 Scheduler.log(`App ${text}`);
1652 return (
1653 <div>
1654 <Suspense fallback={null}>
1655 <Child text={text} />
1656 </Suspense>
1657 </div>
1658 );
1659 }
1660
1661 let spanRef;
1662 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1663 assertLog(['App A', 'Child A']);
1664 const container = document.createElement('div');
1665 document.body.appendChild(container);
1666 container.innerHTML = finalHTML;
1667 const initialSpan = container.getElementsByTagName('span')[0];
1668 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1669 await waitForPaint(['App A']);
1670
1671 await act(() => {
1672 ReactDOM.flushSync(() => {
1673 root.render(<App text="B" />);
1674 });
1675 });
1676 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1677 expect(initialSpan).toBe(spanRef);
1678 });
1679
1680 // @gate www
1681 it('can force hydration in response to continuous update', async () => {
1682 function Child({text}) {
1683 Scheduler.log(`Child ${text}`);
1684 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1685 }
1686 function App({text}) {
1687 Scheduler.log(`App ${text}`);
1688 return (
1689 <div>
1690 <Suspense fallback={null}>
1691 <Child text={text} />
1692 </Suspense>
1693 </div>
1694 );
1695 }
1696
1697 let spanRef;
1698 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1699 assertLog(['App A', 'Child A']);
1700 const container = document.createElement('div');
1701 document.body.appendChild(container);
1702 container.innerHTML = finalHTML;
1703 const initialSpan = container.getElementsByTagName('span')[0];
1704 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1705 await waitForPaint(['App A']);
1706
1707 await act(() => {
1708 TODO_scheduleContinuousSchedulerTask(() => {
1709 root.render(<App text="B" />);
1710 });
1711 });
1712
1713 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1714 expect(initialSpan).toBe(spanRef);
1715 });
1716
1717 it('can force hydration in response to default update', async () => {
1718 function Child({text}) {
1719 Scheduler.log(`Child ${text}`);
1720 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1721 }
1722 function App({text}) {
1723 Scheduler.log(`App ${text}`);
1724 return (
1725 <div>
1726 <Suspense fallback={null}>
1727 <Child text={text} />
1728 </Suspense>
1729 </div>
1730 );
1731 }
1732
1733 let spanRef;
1734 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1735 assertLog(['App A', 'Child A']);
1736 const container = document.createElement('div');
1737 document.body.appendChild(container);
1738 container.innerHTML = finalHTML;
1739 const initialSpan = container.getElementsByTagName('span')[0];
1740 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1741 await waitForPaint(['App A']);
1742 await act(() => {
1743 root.render(<App text="B" />);
1744 });
1745 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1746 expect(initialSpan).toBe(spanRef);
1747 });
1748
1749 // @gate www
1750 it('regression test: can unwind context on selective hydration interruption', async () => {
1751 const Context = React.createContext('DefaultContext');
1752
1753 function ContextReader(props) {
1754 const value = React.useContext(Context);
1755 Scheduler.log(value);
1756 return null;
1757 }
1758
1759 function Child({text}) {
1760 Scheduler.log(text);
1761 return <span>{text}</span>;
1762 }
1763 const ChildWithBoundary = React.memo(function ({text}) {
1764 return (
1765 <Suspense fallback="Loading...">
1766 <Child text={text} />
1767 </Suspense>
1768 );
1769 });
1770
1771 function App({a}) {
1772 Scheduler.log('App');
1773 React.useEffect(() => {
1774 Scheduler.log('Commit');
1775 });
1776 return (
1777 <>
1778 <Context.Provider value="SiblingContext">
1779 <ChildWithBoundary text={a} />
1780 </Context.Provider>
1781 <ContextReader />
1782 </>
1783 );
1784 }
1785 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1786 assertLog(['App', 'A', 'DefaultContext']);
1787 const container = document.createElement('div');
1788 container.innerHTML = finalHTML;
1789 document.body.appendChild(container);
1790
1791 const spanA = container.getElementsByTagName('span')[0];
1792
1793 await act(async () => {
1794 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1795 await waitFor(['App', 'DefaultContext', 'Commit']);
1796
1797 TODO_scheduleIdleDOMSchedulerTask(() => {
1798 root.render(<App a="AA" />);
1799 });
1800 await waitFor(['App', 'A']);
1801
1802 dispatchClickEvent(spanA);
1803 assertLog(['A']);
1804 await waitForAll(['App', 'AA', 'DefaultContext', 'Commit']);
1805 });
1806 });
1807
1808 it('regression test: can unwind context on selective hydration interruption for sync updates', async () => {
1809 const Context = React.createContext('DefaultContext');
1810
1811 function ContextReader(props) {
1812 const value = React.useContext(Context);
1813 Scheduler.log(value);
1814 return null;
1815 }
1816
1817 function Child({text}) {
1818 Scheduler.log(text);
1819 return <span>{text}</span>;
1820 }
1821 const ChildWithBoundary = React.memo(function ({text}) {
1822 return (
1823 <Suspense fallback="Loading...">
1824 <Child text={text} />
1825 </Suspense>
1826 );
1827 });
1828
1829 function App({a}) {
1830 Scheduler.log('App');
1831 React.useEffect(() => {
1832 Scheduler.log('Commit');
1833 });
1834 return (
1835 <>
1836 <Context.Provider value="SiblingContext">
1837 <ChildWithBoundary text={a} />
1838 </Context.Provider>
1839 <ContextReader />
1840 </>
1841 );
1842 }
1843 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1844 assertLog(['App', 'A', 'DefaultContext']);
1845 const container = document.createElement('div');
1846 container.innerHTML = finalHTML;
1847
1848 await act(async () => {
1849 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1850 await waitFor(['App', 'DefaultContext', 'Commit']);
1851
1852 ReactDOM.flushSync(() => {
1853 root.render(<App a="AA" />);
1854 });
1855 assertLog(['App', 'A', 'App', 'AA', 'DefaultContext', 'Commit']);
1856 });
1857 });
1858
1859 it('regression: selective hydration does not contribute to "maximum update limit" count', async () => {
1860 const outsideRef = React.createRef(null);
1861 const insideRef = React.createRef(null);
1862 function Child() {
1863 return (
1864 <Suspense fallback="Loading...">
1865 <div ref={insideRef} />
1866 </Suspense>
1867 );
1868 }
1869
1870 let setIsMounted = false;
1871 function App() {
1872 const [isMounted, setState] = React.useState(false);
1873 setIsMounted = setState;
1874
1875 const children = [];
1876 for (let i = 0; i < 100; i++) {
1877 children.push(<Child key={i} isMounted={isMounted} />);
1878 }
1879
1880 return <div ref={outsideRef}>{children}</div>;
1881 }
1882
1883 const finalHTML = ReactDOMServer.renderToString(<App />);
1884 const container = document.createElement('div');
1885 container.innerHTML = finalHTML;
1886
1887 await act(async () => {
1888 ReactDOMClient.hydrateRoot(container, <App />);
1889
1890 // Commit just the shell
1891 await waitForPaint([]);
1892
1893 // Assert that the shell has hydrated, but not the children
1894 expect(outsideRef.current).not.toBe(null);
1895 expect(insideRef.current).toBe(null);
1896
1897 // Update the shell synchronously. The update will flow into the children,
1898 // which haven't hydrated yet. This will trigger a cascade of commits
1899 // caused by selective hydration. However, since there's really only one
1900 // update, it should not be treated as an update loop.
1901 // NOTE: It's unfortunate that every sibling boundary is separately
1902 // committed in this case. We should be able to commit everything in a
1903 // render phase, which we could do if we had resumable context stacks.
1904 ReactDOM.flushSync(() => {
1905 setIsMounted(true);
1906 });
1907 });
1908
1909 // Should have successfully hydrated with no errors.
1910 expect(insideRef.current).not.toBe(null);
1911 });
1912 });