main
js 1,595 lines 41.4 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 Activity;
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('ReactDOMServerSelectiveHydrationActivity', () => {
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 Activity = React.Activity;
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 <Activity>
174 <Child text="A" />
175 </Activity>
176 <Activity>
177 <Child text="B" />
178 </Activity>
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 <Activity>
242 <Child text="A" />
243 </Activity>
244 <Activity>
245 <Child text="B" />
246 </Activity>
247 <Activity>
248 <Child text="C" />
249 </Activity>
250 <Activity>
251 <Child text="D" />
252 </Activity>
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 <Activity>
327 <Child text="A" />
328 </Activity>
329 <Activity>
330 <Child text="B" />
331 </Activity>
332 <Activity>
333 <Child text="C" />
334 </Activity>
335 <Activity>
336 <Child text="D" />
337 </Activity>
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 <Activity>
412 <Child text="A" />
413 </Activity>
414 <Activity>
415 <Child text="B" />
416 </Activity>
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 <Activity>
486 <Child text="A" />
487 </Activity>
488 <Activity>
489 <Child text="B" />
490 </Activity>
491 <Activity>
492 <Child text="C" />
493 </Activity>
494 <Activity>
495 <Child text="D" />
496 </Activity>
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 <Activity>
574 <Child text="A" />
575 </Activity>
576 <Activity>
577 <Child text="B" />
578 </Activity>
579 <Activity>
580 <Child text="C" />
581 </Activity>
582 <Activity>
583 <Child text="D" />
584 </Activity>
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 <Activity>
665 <Child text="A" />
666 </Activity>
667 <Activity>
668 <Child text="B" />
669 </Activity>
670 <Activity>
671 <Child text="C" />
672 </Activity>
673 <Activity>
674 <Child text="D" />
675 </Activity>
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 <Activity>
794 <Child text="A" />
795 </Activity>
796 <Activity>
797 <Child text="B" />
798 </Activity>
799 <Activity>
800 <Child text="C" />
801 </Activity>
802 <Activity>
803 <Child text="D" />
804 </Activity>
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 it('replays event with null target when tree is dismounted', async () => {
882 let suspend = false;
883 let resolve;
884 const promise = new Promise(resolvePromise => {
885 resolve = () => {
886 suspend = false;
887 resolvePromise();
888 };
889 });
890
891 function Child() {
892 if (suspend) {
893 throw promise;
894 }
895 Scheduler.log('Child');
896 return (
897 <div
898 onMouseOver={() => {
899 Scheduler.log('on mouse over');
900 }}>
901 Child
902 </div>
903 );
904 }
905
906 function App() {
907 return (
908 <Activity>
909 <Child />
910 </Activity>
911 );
912 }
913
914 const finalHTML = ReactDOMServer.renderToString(<App />);
915 assertLog(['Child']);
916
917 const container = document.createElement('div');
918
919 document.body.appendChild(container);
920 container.innerHTML = finalHTML;
921 suspend = true;
922
923 ReactDOMClient.hydrateRoot(container, <App />);
924
925 const childDiv = container.firstElementChild;
926
927 await act(async () => {
928 dispatchMouseHoverEvent(childDiv);
929
930 // Not hydrated so event is saved for replay and stopPropagation is called
931 assertLog([]);
932
933 resolve();
934 await waitFor(['Child']);
935
936 ReactDOM.flushSync(() => {
937 container.removeChild(childDiv);
938
939 const container2 = document.createElement('div');
940 container2.addEventListener('mouseover', () => {
941 Scheduler.log('container2 mouse over');
942 });
943 container2.appendChild(childDiv);
944 });
945 });
946
947 // Even though the tree is remove the event is still dispatched with native event handler
948 // on the container firing.
949 assertLog(['container2 mouse over']);
950
951 document.body.removeChild(container);
952 });
953
954 it('hydrates the last target path first for continuous events', async () => {
955 let suspend = false;
956 let resolve;
957 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
958
959 function Child({text}) {
960 if ((text === 'A' || text === 'D') && suspend) {
961 throw promise;
962 }
963 Scheduler.log(text);
964 return (
965 <span
966 onMouseEnter={e => {
967 e.preventDefault();
968 Scheduler.log('Hover ' + text);
969 }}>
970 {text}
971 </span>
972 );
973 }
974
975 function App() {
976 Scheduler.log('App');
977 return (
978 <div>
979 <Activity>
980 <Child text="A" />
981 </Activity>
982 <Activity>
983 <div>
984 <Activity>
985 <Child text="B" />
986 </Activity>
987 </div>
988 <Child text="C" />
989 </Activity>
990 <Activity>
991 <Child text="D" />
992 </Activity>
993 </div>
994 );
995 }
996
997 const finalHTML = ReactDOMServer.renderToString(<App />);
998
999 assertLog(['App', 'A', 'B', 'C', 'D']);
1000
1001 const container = document.createElement('div');
1002 // We need this to be in the document since we'll dispatch events on it.
1003 document.body.appendChild(container);
1004
1005 container.innerHTML = finalHTML;
1006
1007 const spanB = container.getElementsByTagName('span')[1];
1008 const spanC = container.getElementsByTagName('span')[2];
1009 const spanD = container.getElementsByTagName('span')[3];
1010
1011 suspend = true;
1012
1013 // A and D will be suspended. We'll click on D which should take
1014 // priority, after we unsuspend.
1015 ReactDOMClient.hydrateRoot(container, <App />);
1016
1017 // Nothing has been hydrated so far.
1018 assertLog([]);
1019
1020 // Hover over B and then C.
1021 dispatchMouseHoverEvent(spanB, spanD);
1022 dispatchMouseHoverEvent(spanC, spanB);
1023
1024 await act(async () => {
1025 suspend = false;
1026 resolve();
1027 await promise;
1028 });
1029
1030 // We should prioritize hydrating D first because we clicked it.
1031 // Next we should hydrate C since that's the current hover target.
1032 // Next it doesn't matter if we hydrate A or B first but as an
1033 // implementation detail we're currently hydrating B first since
1034 // we at one point hovered over it and we never deprioritized it.
1035 assertLog(['App', 'C', 'Hover C', 'A', 'B', 'D']);
1036
1037 document.body.removeChild(container);
1038 });
1039
1040 it('hydrates the last explicitly hydrated target at higher priority', async () => {
1041 function Child({text}) {
1042 Scheduler.log(text);
1043 return <span>{text}</span>;
1044 }
1045
1046 function App() {
1047 Scheduler.log('App');
1048 return (
1049 <div>
1050 <Activity>
1051 <Child text="A" />
1052 </Activity>
1053 <Activity>
1054 <Child text="B" />
1055 </Activity>
1056 <Activity>
1057 <Child text="C" />
1058 </Activity>
1059 </div>
1060 );
1061 }
1062
1063 const finalHTML = ReactDOMServer.renderToString(<App />);
1064
1065 assertLog(['App', 'A', 'B', 'C']);
1066
1067 const container = document.createElement('div');
1068 container.innerHTML = finalHTML;
1069
1070 const spanB = container.getElementsByTagName('span')[1];
1071 const spanC = container.getElementsByTagName('span')[2];
1072
1073 const root = ReactDOMClient.hydrateRoot(container, <App />);
1074
1075 // Nothing has been hydrated so far.
1076 assertLog([]);
1077
1078 // Increase priority of B and then C.
1079 root.unstable_scheduleHydration(spanB);
1080 root.unstable_scheduleHydration(spanC);
1081
1082 // We should prioritize hydrating C first because the last added
1083 // gets highest priority followed by the next added.
1084 await waitForAll(['App', 'C', 'B', 'A']);
1085 });
1086
1087 // @gate www
1088 it('hydrates before an update even if hydration moves away from it', async () => {
1089 function Child({text}) {
1090 Scheduler.log(text);
1091 return <span>{text}</span>;
1092 }
1093 const ChildWithBoundary = React.memo(function ({text}) {
1094 return (
1095 <Activity>
1096 <Child text={text} />
1097 <Child text={text.toLowerCase()} />
1098 </Activity>
1099 );
1100 });
1101
1102 function App({a}) {
1103 Scheduler.log('App');
1104 React.useEffect(() => {
1105 Scheduler.log('Commit');
1106 });
1107 return (
1108 <div>
1109 <ChildWithBoundary text={a} />
1110 <ChildWithBoundary text="B" />
1111 <ChildWithBoundary text="C" />
1112 </div>
1113 );
1114 }
1115
1116 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1117
1118 assertLog(['App', 'A', 'a', 'B', 'b', 'C', 'c']);
1119
1120 const container = document.createElement('div');
1121 container.innerHTML = finalHTML;
1122
1123 // We need this to be in the document since we'll dispatch events on it.
1124 document.body.appendChild(container);
1125
1126 const spanA = container.getElementsByTagName('span')[0];
1127 const spanB = container.getElementsByTagName('span')[2];
1128 const spanC = container.getElementsByTagName('span')[4];
1129
1130 await act(async () => {
1131 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1132 // Hydrate the shell.
1133 await waitFor(['App', 'Commit']);
1134
1135 // Render an update at Idle priority that needs to update A.
1136
1137 TODO_scheduleIdleDOMSchedulerTask(() => {
1138 root.render(<App a="AA" />);
1139 });
1140
1141 // Start rendering. This will force the first boundary to hydrate
1142 // by scheduling it at one higher pri than Idle.
1143 await waitFor([
1144 'App',
1145
1146 // Start hydrating A
1147 'A',
1148 ]);
1149
1150 // Hover over A which (could) schedule at one higher pri than Idle.
1151 dispatchMouseHoverEvent(spanA, null);
1152
1153 // Before, we're done we now switch to hover over B.
1154 // This is meant to test that this doesn't cause us to forget that
1155 // we still have to hydrate A. The first boundary.
1156 // This also tests that we don't do the -1 down-prioritization of
1157 // continuous hover events because that would decrease its priority
1158 // to Idle.
1159 dispatchMouseHoverEvent(spanB, spanA);
1160
1161 // Also click C to prioritize that even higher which resets the
1162 // priority levels.
1163 dispatchClickEvent(spanC);
1164
1165 assertLog([
1166 // Hydrate C first since we clicked it.
1167 'C',
1168 'c',
1169 ]);
1170
1171 await waitForAll([
1172 // Finish hydration of A since we forced it to hydrate.
1173 'A',
1174 'a',
1175 // Also, hydrate B since we hovered over it.
1176 // It's not important which one comes first. A or B.
1177 // As long as they both happen before the Idle update.
1178 'B',
1179 'b',
1180 // Begin the Idle update again.
1181 'App',
1182 'AA',
1183 'aa',
1184 'Commit',
1185 ]);
1186 });
1187
1188 const spanA2 = container.getElementsByTagName('span')[0];
1189 // This is supposed to have been hydrated, not replaced.
1190 expect(spanA).toBe(spanA2);
1191
1192 document.body.removeChild(container);
1193 });
1194
1195 it('fires capture event handlers and native events if content is hydratable during discrete event', async () => {
1196 spyOnDev(console, 'error');
1197 function Child({text}) {
1198 Scheduler.log(text);
1199 const ref = React.useRef();
1200 React.useLayoutEffect(() => {
1201 if (!ref.current) {
1202 return;
1203 }
1204 ref.current.onclick = () => {
1205 Scheduler.log('Native Click ' + text);
1206 };
1207 }, [text]);
1208 return (
1209 <span
1210 ref={ref}
1211 onClickCapture={() => {
1212 Scheduler.log('Capture Clicked ' + text);
1213 }}
1214 onClick={e => {
1215 Scheduler.log('Clicked ' + text);
1216 }}>
1217 {text}
1218 </span>
1219 );
1220 }
1221
1222 function App() {
1223 Scheduler.log('App');
1224 return (
1225 <div>
1226 <Activity>
1227 <Child text="A" />
1228 </Activity>
1229 <Activity>
1230 <Child text="B" />
1231 </Activity>
1232 </div>
1233 );
1234 }
1235
1236 const finalHTML = ReactDOMServer.renderToString(<App />);
1237
1238 assertLog(['App', 'A', 'B']);
1239
1240 const container = document.createElement('div');
1241 // We need this to be in the document since we'll dispatch events on it.
1242 document.body.appendChild(container);
1243
1244 container.innerHTML = finalHTML;
1245
1246 const span = container.getElementsByTagName('span')[1];
1247
1248 ReactDOMClient.hydrateRoot(container, <App />);
1249
1250 // Nothing has been hydrated so far.
1251 assertLog([]);
1252
1253 // This should synchronously hydrate the root App and the second suspense
1254 // boundary.
1255 dispatchClickEvent(span);
1256
1257 // We rendered App, B and then invoked the event without rendering A.
1258 assertLog(['App', 'B', 'Capture Clicked B', 'Native Click B', 'Clicked B']);
1259
1260 // After continuing the scheduler, we finally hydrate A.
1261 await waitForAll(['A']);
1262
1263 document.body.removeChild(container);
1264 });
1265
1266 it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
1267 let triggeredParent = false;
1268 let triggeredChild = false;
1269 let suspend = false;
1270 const promise = new Promise(() => {});
1271 function Child() {
1272 if (suspend) {
1273 throw promise;
1274 }
1275 Scheduler.log('Child');
1276 return (
1277 <span
1278 onClickCapture={e => {
1279 e.stopPropagation();
1280 triggeredChild = true;
1281 }}>
1282 Click me
1283 </span>
1284 );
1285 }
1286 function App() {
1287 const onClick = () => {
1288 triggeredParent = true;
1289 };
1290 Scheduler.log('App');
1291 return (
1292 <div
1293 ref={n => {
1294 if (n) n.onclick = onClick;
1295 }}
1296 onClick={onClick}>
1297 <Activity>
1298 <Child />
1299 </Activity>
1300 </div>
1301 );
1302 }
1303 const finalHTML = ReactDOMServer.renderToString(<App />);
1304
1305 assertLog(['App', 'Child']);
1306
1307 const container = document.createElement('div');
1308 document.body.appendChild(container);
1309 container.innerHTML = finalHTML;
1310
1311 suspend = true;
1312
1313 ReactDOMClient.hydrateRoot(container, <App />);
1314 // Nothing has been hydrated so far.
1315 assertLog([]);
1316
1317 const span = container.getElementsByTagName('span')[0];
1318 dispatchClickEvent(span);
1319
1320 assertLog(['App']);
1321
1322 dispatchClickEvent(span);
1323
1324 expect(triggeredParent).toBe(false);
1325 expect(triggeredChild).toBe(false);
1326 });
1327
1328 it('can force hydration in response to sync update', async () => {
1329 function Child({text}) {
1330 Scheduler.log(`Child ${text}`);
1331 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1332 }
1333 function App({text}) {
1334 Scheduler.log(`App ${text}`);
1335 return (
1336 <div>
1337 <Activity>
1338 <Child text={text} />
1339 </Activity>
1340 </div>
1341 );
1342 }
1343
1344 let spanRef;
1345 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1346 assertLog(['App A', 'Child A']);
1347 const container = document.createElement('div');
1348 document.body.appendChild(container);
1349 container.innerHTML = finalHTML;
1350 const initialSpan = container.getElementsByTagName('span')[0];
1351 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1352 await waitForPaint(['App A']);
1353
1354 await act(() => {
1355 ReactDOM.flushSync(() => {
1356 root.render(<App text="B" />);
1357 });
1358 });
1359 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1360 expect(initialSpan).toBe(spanRef);
1361 });
1362
1363 // @gate www
1364 it('can force hydration in response to continuous update', async () => {
1365 function Child({text}) {
1366 Scheduler.log(`Child ${text}`);
1367 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1368 }
1369 function App({text}) {
1370 Scheduler.log(`App ${text}`);
1371 return (
1372 <div>
1373 <Activity>
1374 <Child text={text} />
1375 </Activity>
1376 </div>
1377 );
1378 }
1379
1380 let spanRef;
1381 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1382 assertLog(['App A', 'Child A']);
1383 const container = document.createElement('div');
1384 document.body.appendChild(container);
1385 container.innerHTML = finalHTML;
1386 const initialSpan = container.getElementsByTagName('span')[0];
1387 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1388 await waitForPaint(['App A']);
1389
1390 await act(() => {
1391 TODO_scheduleContinuousSchedulerTask(() => {
1392 root.render(<App text="B" />);
1393 });
1394 });
1395
1396 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1397 expect(initialSpan).toBe(spanRef);
1398 });
1399
1400 it('can force hydration in response to default update', async () => {
1401 function Child({text}) {
1402 Scheduler.log(`Child ${text}`);
1403 return <span ref={ref => (spanRef = ref)}>{text}</span>;
1404 }
1405 function App({text}) {
1406 Scheduler.log(`App ${text}`);
1407 return (
1408 <div>
1409 <Activity>
1410 <Child text={text} />
1411 </Activity>
1412 </div>
1413 );
1414 }
1415
1416 let spanRef;
1417 const finalHTML = ReactDOMServer.renderToString(<App text="A" />);
1418 assertLog(['App A', 'Child A']);
1419 const container = document.createElement('div');
1420 document.body.appendChild(container);
1421 container.innerHTML = finalHTML;
1422 const initialSpan = container.getElementsByTagName('span')[0];
1423 const root = ReactDOMClient.hydrateRoot(container, <App text="A" />);
1424 await waitForPaint(['App A']);
1425 await act(() => {
1426 root.render(<App text="B" />);
1427 });
1428 assertLog(['App B', 'Child A', 'App B', 'Child B']);
1429 expect(initialSpan).toBe(spanRef);
1430 });
1431
1432 // @gate www
1433 it('regression test: can unwind context on selective hydration interruption', async () => {
1434 const Context = React.createContext('DefaultContext');
1435
1436 function ContextReader(props) {
1437 const value = React.useContext(Context);
1438 Scheduler.log(value);
1439 return null;
1440 }
1441
1442 function Child({text}) {
1443 Scheduler.log(text);
1444 return <span>{text}</span>;
1445 }
1446 const ChildWithBoundary = React.memo(function ({text}) {
1447 return (
1448 <Activity>
1449 <Child text={text} />
1450 </Activity>
1451 );
1452 });
1453
1454 function App({a}) {
1455 Scheduler.log('App');
1456 React.useEffect(() => {
1457 Scheduler.log('Commit');
1458 });
1459 return (
1460 <>
1461 <Context.Provider value="SiblingContext">
1462 <ChildWithBoundary text={a} />
1463 </Context.Provider>
1464 <ContextReader />
1465 </>
1466 );
1467 }
1468 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1469 assertLog(['App', 'A', 'DefaultContext']);
1470 const container = document.createElement('div');
1471 container.innerHTML = finalHTML;
1472 document.body.appendChild(container);
1473
1474 const spanA = container.getElementsByTagName('span')[0];
1475
1476 await act(async () => {
1477 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1478 await waitFor(['App', 'DefaultContext', 'Commit']);
1479
1480 TODO_scheduleIdleDOMSchedulerTask(() => {
1481 root.render(<App a="AA" />);
1482 });
1483 await waitFor(['App', 'A']);
1484
1485 dispatchClickEvent(spanA);
1486 assertLog(['A']);
1487 await waitForAll(['App', 'AA', 'DefaultContext', 'Commit']);
1488 });
1489 });
1490
1491 it('regression test: can unwind context on selective hydration interruption for sync updates', async () => {
1492 const Context = React.createContext('DefaultContext');
1493
1494 function ContextReader(props) {
1495 const value = React.useContext(Context);
1496 Scheduler.log(value);
1497 return null;
1498 }
1499
1500 function Child({text}) {
1501 Scheduler.log(text);
1502 return <span>{text}</span>;
1503 }
1504 const ChildWithBoundary = React.memo(function ({text}) {
1505 return (
1506 <Activity>
1507 <Child text={text} />
1508 </Activity>
1509 );
1510 });
1511
1512 function App({a}) {
1513 Scheduler.log('App');
1514 React.useEffect(() => {
1515 Scheduler.log('Commit');
1516 });
1517 return (
1518 <>
1519 <Context.Provider value="SiblingContext">
1520 <ChildWithBoundary text={a} />
1521 </Context.Provider>
1522 <ContextReader />
1523 </>
1524 );
1525 }
1526 const finalHTML = ReactDOMServer.renderToString(<App a="A" />);
1527 assertLog(['App', 'A', 'DefaultContext']);
1528 const container = document.createElement('div');
1529 container.innerHTML = finalHTML;
1530
1531 await act(async () => {
1532 const root = ReactDOMClient.hydrateRoot(container, <App a="A" />);
1533 await waitFor(['App', 'DefaultContext', 'Commit']);
1534
1535 ReactDOM.flushSync(() => {
1536 root.render(<App a="AA" />);
1537 });
1538 assertLog(['App', 'A', 'App', 'AA', 'DefaultContext', 'Commit']);
1539 });
1540 });
1541
1542 it('regression: selective hydration does not contribute to "maximum update limit" count', async () => {
1543 const outsideRef = React.createRef(null);
1544 const insideRef = React.createRef(null);
1545 function Child() {
1546 return (
1547 <Activity>
1548 <div ref={insideRef} />
1549 </Activity>
1550 );
1551 }
1552
1553 let setIsMounted = false;
1554 function App() {
1555 const [isMounted, setState] = React.useState(false);
1556 setIsMounted = setState;
1557
1558 const children = [];
1559 for (let i = 0; i < 100; i++) {
1560 children.push(<Child key={i} isMounted={isMounted} />);
1561 }
1562
1563 return <div ref={outsideRef}>{children}</div>;
1564 }
1565
1566 const finalHTML = ReactDOMServer.renderToString(<App />);
1567 const container = document.createElement('div');
1568 container.innerHTML = finalHTML;
1569
1570 await act(async () => {
1571 ReactDOMClient.hydrateRoot(container, <App />);
1572
1573 // Commit just the shell
1574 await waitForPaint([]);
1575
1576 // Assert that the shell has hydrated, but not the children
1577 expect(outsideRef.current).not.toBe(null);
1578 expect(insideRef.current).toBe(null);
1579
1580 // Update the shell synchronously. The update will flow into the children,
1581 // which haven't hydrated yet. This will trigger a cascade of commits
1582 // caused by selective hydration. However, since there's really only one
1583 // update, it should not be treated as an update loop.
1584 // NOTE: It's unfortunate that every sibling boundary is separately
1585 // committed in this case. We should be able to commit everything in a
1586 // render phase, which we could do if we had resumable context stacks.
1587 ReactDOM.flushSync(() => {
1588 setIsMounted(true);
1589 });
1590 });
1591
1592 // Should have successfully hydrated with no errors.
1593 expect(insideRef.current).not.toBe(null);
1594 });
1595 });