@@ -14,6 +14,7 @@ import {createEventTarget} from 'dom-event-testing-library';
14
14
let React ;
15
15
let ReactDOM ;
16
16
let ReactDOMServer ;
17
+ let ReactTestUtils ;
17
18
let Scheduler ;
18
19
let Suspense ;
19
20
let usePress ;
@@ -102,6 +103,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
102
103
React = require ( 'react' ) ;
103
104
ReactDOM = require ( 'react-dom' ) ;
104
105
ReactDOMServer = require ( 'react-dom/server' ) ;
106
+ ReactTestUtils = require ( 'react-dom/test-utils' ) ;
105
107
Scheduler = require ( 'scheduler' ) ;
106
108
Suspense = React . Suspense ;
107
109
usePress = require ( 'react-interactions/events/press' ) . usePress ;
@@ -585,7 +587,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
585
587
document . body . removeChild ( container ) ;
586
588
} ) ;
587
589
588
- it ( 'hydrates the last target as higher priority for continuous events' , async ( ) => {
590
+ it ( 'hydrates the hovered targets as higher priority for continuous events' , async ( ) => {
589
591
let suspend = false ;
590
592
let resolve ;
591
593
let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
@@ -669,21 +671,107 @@ describe('ReactDOMServerSelectiveHydration', () => {
669
671
670
672
// We should prioritize hydrating D first because we clicked it.
671
673
// Next we should hydrate C since that's the current hover target.
672
- // Next it doesn't matter if we hydrate A or B first but as an
673
- // implementation detail we're currently hydrating B first since
674
- // we at one point hovered over it and we never deprioritized it.
674
+ // To simplify implementation details we hydrate both B and C at
675
+ // the same time since B was already scheduled.
676
+ // This is ok because it will at least not continue for nested
677
+ // boundary. See the next test below.
675
678
expect ( Scheduler ) . toFlushAndYield ( [
676
679
'D' ,
677
680
'Clicked D' ,
681
+ 'B' , // Ideally this should be later.
678
682
'C' ,
679
683
'Hover C' ,
680
- 'B' ,
681
684
'A' ,
682
685
] ) ;
683
686
684
687
document . body . removeChild ( container ) ;
685
688
} ) ;
686
689
690
+ it ( 'hydrates the last target path first for continuous events' , async ( ) => {
691
+ let suspend = false ;
692
+ let resolve ;
693
+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
694
+
695
+ function Child ( { text} ) {
696
+ if ( ( text === 'A' || text === 'D' ) && suspend ) {
697
+ throw promise ;
698
+ }
699
+ Scheduler . unstable_yieldValue ( text ) ;
700
+ return (
701
+ < span
702
+ onMouseEnter = { e => {
703
+ e . preventDefault ( ) ;
704
+ Scheduler . unstable_yieldValue ( 'Hover ' + text ) ;
705
+ } } >
706
+ { text }
707
+ </ span >
708
+ ) ;
709
+ }
710
+
711
+ function App ( ) {
712
+ Scheduler . unstable_yieldValue ( 'App' ) ;
713
+ return (
714
+ < div >
715
+ < Suspense fallback = "Loading..." >
716
+ < Child text = "A" />
717
+ </ Suspense >
718
+ < Suspense fallback = "Loading..." >
719
+ < div >
720
+ < Suspense fallback = "Loading..." >
721
+ < Child text = "B" />
722
+ </ Suspense >
723
+ </ div >
724
+ < Child text = "C" />
725
+ </ Suspense >
726
+ < Suspense fallback = "Loading..." >
727
+ < Child text = "D" />
728
+ </ Suspense >
729
+ </ div >
730
+ ) ;
731
+ }
732
+
733
+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
734
+
735
+ expect ( Scheduler ) . toHaveYielded ( [ 'App' , 'A' , 'B' , 'C' , 'D' ] ) ;
736
+
737
+ let container = document . createElement ( 'div' ) ;
738
+ // We need this to be in the document since we'll dispatch events on it.
739
+ document . body . appendChild ( container ) ;
740
+
741
+ container . innerHTML = finalHTML ;
742
+
743
+ let spanB = container . getElementsByTagName ( 'span' ) [ 1 ] ;
744
+ let spanC = container . getElementsByTagName ( 'span' ) [ 2 ] ;
745
+ let spanD = container . getElementsByTagName ( 'span' ) [ 3 ] ;
746
+
747
+ suspend = true ;
748
+
749
+ // A and D will be suspended. We'll click on D which should take
750
+ // priority, after we unsuspend.
751
+ let root = ReactDOM . createRoot ( container , { hydrate : true } ) ;
752
+ root . render ( < App /> ) ;
753
+
754
+ // Nothing has been hydrated so far.
755
+ expect ( Scheduler ) . toHaveYielded ( [ ] ) ;
756
+
757
+ // Hover over B and then C.
758
+ dispatchMouseHoverEvent ( spanB , spanD ) ;
759
+ dispatchMouseHoverEvent ( spanC , spanB ) ;
760
+
761
+ suspend = false ;
762
+ resolve ( ) ;
763
+ await promise ;
764
+
765
+ // We should prioritize hydrating D first because we clicked it.
766
+ // Next we should hydrate C since that's the current hover target.
767
+ // Next it doesn't matter if we hydrate A or B first but as an
768
+ // implementation detail we're currently hydrating B first since
769
+ // we at one point hovered over it and we never deprioritized it.
770
+ expect ( Scheduler ) . toFlushAndYield ( [ 'App' , 'C' , 'Hover C' , 'A' , 'B' , 'D' ] ) ;
771
+
772
+ document . body . removeChild ( container ) ;
773
+ } ) ;
774
+
687
775
it ( 'hydrates the last explicitly hydrated target at higher priority' , async ( ) => {
688
776
function Child ( { text} ) {
689
777
Scheduler . unstable_yieldValue ( text ) ;
@@ -731,4 +819,110 @@ describe('ReactDOMServerSelectiveHydration', () => {
731
819
// gets highest priority followed by the next added.
732
820
expect ( Scheduler ) . toFlushAndYield ( [ 'App' , 'C' , 'B' , 'A' ] ) ;
733
821
} ) ;
822
+
823
+ it ( 'hydrates before an update even if hydration moves away from it' , async ( ) => {
824
+ function Child ( { text} ) {
825
+ Scheduler . unstable_yieldValue ( text ) ;
826
+ return < span > { text } </ span > ;
827
+ }
828
+ let ChildWithBoundary = React . memo ( function ( { text} ) {
829
+ return (
830
+ < Suspense fallback = "Loading..." >
831
+ < Child text = { text } />
832
+ < Child text = { text . toLowerCase ( ) } />
833
+ </ Suspense >
834
+ ) ;
835
+ } ) ;
836
+
837
+ function App ( { a} ) {
838
+ Scheduler . unstable_yieldValue ( 'App' ) ;
839
+ React . useEffect ( ( ) => {
840
+ Scheduler . unstable_yieldValue ( 'Commit' ) ;
841
+ } ) ;
842
+ return (
843
+ < div >
844
+ < ChildWithBoundary text = { a } />
845
+ < ChildWithBoundary text = "B" />
846
+ < ChildWithBoundary text = "C" />
847
+ </ div >
848
+ ) ;
849
+ }
850
+
851
+ let finalHTML = ReactDOMServer . renderToString ( < App a = "A" /> ) ;
852
+
853
+ expect ( Scheduler ) . toHaveYielded ( [ 'App' , 'A' , 'a' , 'B' , 'b' , 'C' , 'c' ] ) ;
854
+
855
+ let container = document . createElement ( 'div' ) ;
856
+ container . innerHTML = finalHTML ;
857
+
858
+ // We need this to be in the document since we'll dispatch events on it.
859
+ document . body . appendChild ( container ) ;
860
+
861
+ let spanA = container . getElementsByTagName ( 'span' ) [ 0 ] ;
862
+ let spanB = container . getElementsByTagName ( 'span' ) [ 2 ] ;
863
+ let spanC = container . getElementsByTagName ( 'span' ) [ 4 ] ;
864
+
865
+ let root = ReactDOM . createRoot ( container , { hydrate : true } ) ;
866
+ ReactTestUtils . act ( ( ) => {
867
+ root . render ( < App a = "A" /> ) ;
868
+
869
+ // Hydrate the shell.
870
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [ 'App' , 'Commit' ] ) ;
871
+
872
+ // Render an update at Idle priority that needs to update A.
873
+ Scheduler . unstable_runWithPriority (
874
+ Scheduler . unstable_IdlePriority ,
875
+ ( ) => {
876
+ root . render ( < App a = "AA" /> ) ;
877
+ } ,
878
+ ) ;
879
+
880
+ // Start rendering. This will force the first boundary to hydrate
881
+ // by scheduling it at one higher pri than Idle.
882
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [ 'App' , 'A' ] ) ;
883
+
884
+ // Hover over A which (could) schedule at one higher pri than Idle.
885
+ dispatchMouseHoverEvent ( spanA , null ) ;
886
+
887
+ // Before, we're done we now switch to hover over B.
888
+ // This is meant to test that this doesn't cause us to forget that
889
+ // we still have to hydrate A. The first boundary.
890
+ // This also tests that we don't do the -1 down-prioritization of
891
+ // continuous hover events because that would decrease its priority
892
+ // to Idle.
893
+ dispatchMouseHoverEvent ( spanB , spanA ) ;
894
+
895
+ // Also click C to prioritize that even higher which resets the
896
+ // priority levels.
897
+ dispatchClickEvent ( spanC ) ;
898
+
899
+ expect ( Scheduler ) . toHaveYielded ( [
900
+ // Hydrate C first since we clicked it.
901
+ 'C' ,
902
+ 'c' ,
903
+ ] ) ;
904
+
905
+ expect ( Scheduler ) . toFlushAndYield ( [
906
+ // Finish hydration of A since we forced it to hydrate.
907
+ 'A' ,
908
+ 'a' ,
909
+ // Also, hydrate B since we hovered over it.
910
+ // It's not important which one comes first. A or B.
911
+ // As long as they both happen before the Idle update.
912
+ 'B' ,
913
+ 'b' ,
914
+ // Begin the Idle update again.
915
+ 'App' ,
916
+ 'AA' ,
917
+ 'aa' ,
918
+ 'Commit' ,
919
+ ] ) ;
920
+ } ) ;
921
+
922
+ let spanA2 = container . getElementsByTagName ( 'span' ) [ 0 ] ;
923
+ // This is supposed to have been hydrated, not replaced.
924
+ expect ( spanA ) . toBe ( spanA2 ) ;
925
+
926
+ document . body . removeChild ( container ) ;
927
+ } ) ;
734
928
} ) ;
0 commit comments