@@ -114,4 +114,180 @@ describe('ReactDOMServerSelectiveHydration', () => {
114114
115115 document . body . removeChild ( container ) ;
116116 } ) ;
117+
118+ it ( 'hydrates at higher pri if sync did not work first time' , async ( ) => {
119+ let suspend = false ;
120+ let resolve ;
121+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
122+
123+ function Child ( { text} ) {
124+ if ( ( text === 'A' || text === 'D' ) && suspend ) {
125+ throw promise ;
126+ }
127+ Scheduler . unstable_yieldValue ( text ) ;
128+ return (
129+ < span
130+ onClick = { e => {
131+ e . preventDefault ( ) ;
132+ Scheduler . unstable_yieldValue ( 'Clicked ' + text ) ;
133+ } } >
134+ { text }
135+ </ span >
136+ ) ;
137+ }
138+
139+ function App ( ) {
140+ Scheduler . unstable_yieldValue ( 'App' ) ;
141+ return (
142+ < div >
143+ < Suspense fallback = "Loading..." >
144+ < Child text = "A" />
145+ </ Suspense >
146+ < Suspense fallback = "Loading..." >
147+ < Child text = "B" />
148+ </ Suspense >
149+ < Suspense fallback = "Loading..." >
150+ < Child text = "C" />
151+ </ Suspense >
152+ < Suspense fallback = "Loading..." >
153+ < Child text = "D" />
154+ </ Suspense >
155+ </ div >
156+ ) ;
157+ }
158+
159+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
160+
161+ expect ( Scheduler ) . toHaveYielded ( [ 'App' , 'A' , 'B' , 'C' , 'D' ] ) ;
162+
163+ let container = document . createElement ( 'div' ) ;
164+ // We need this to be in the document since we'll dispatch events on it.
165+ document . body . appendChild ( container ) ;
166+
167+ container . innerHTML = finalHTML ;
168+
169+ let spanD = container . getElementsByTagName ( 'span' ) [ 3 ] ;
170+
171+ suspend = true ;
172+
173+ // A and D will be suspended. We'll click on D which should take
174+ // priority, after we unsuspend.
175+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
176+ root . render ( < App /> ) ;
177+
178+ // Nothing has been hydrated so far.
179+ expect ( Scheduler ) . toHaveYielded ( [ ] ) ;
180+
181+ // This click target cannot be hydrated yet because it's suspended.
182+ let result = dispatchClickEvent ( spanD ) ;
183+
184+ expect ( Scheduler ) . toHaveYielded ( [ 'App' ] ) ;
185+
186+ expect ( result ) . toBe ( true ) ;
187+
188+ // Continuing rendering will render B next.
189+ expect ( Scheduler ) . toFlushAndYield ( [ 'B' , 'C' ] ) ;
190+
191+ suspend = false ;
192+ resolve ( ) ;
193+ await promise ;
194+
195+ // After the click, we should prioritize D and the Click first,
196+ // and only after that render A and C.
197+ expect ( Scheduler ) . toFlushAndYield ( [ 'D' , 'Clicked D' , 'A' ] ) ;
198+
199+ document . body . removeChild ( container ) ;
200+ } ) ;
201+
202+ it ( 'hydrates at higher pri for secondary discrete events' , async ( ) => {
203+ let suspend = false ;
204+ let resolve ;
205+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
206+
207+ function Child ( { text} ) {
208+ if ( ( text === 'A' || text === 'D' ) && suspend ) {
209+ throw promise ;
210+ }
211+ Scheduler . unstable_yieldValue ( text ) ;
212+ return (
213+ < span
214+ onClick = { e => {
215+ e . preventDefault ( ) ;
216+ Scheduler . unstable_yieldValue ( 'Clicked ' + text ) ;
217+ } } >
218+ { text }
219+ </ span >
220+ ) ;
221+ }
222+
223+ function App ( ) {
224+ Scheduler . unstable_yieldValue ( 'App' ) ;
225+ return (
226+ < div >
227+ < Suspense fallback = "Loading..." >
228+ < Child text = "A" />
229+ </ Suspense >
230+ < Suspense fallback = "Loading..." >
231+ < Child text = "B" />
232+ </ Suspense >
233+ < Suspense fallback = "Loading..." >
234+ < Child text = "C" />
235+ </ Suspense >
236+ < Suspense fallback = "Loading..." >
237+ < Child text = "D" />
238+ </ Suspense >
239+ </ div >
240+ ) ;
241+ }
242+
243+ let finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
244+
245+ expect ( Scheduler ) . toHaveYielded ( [ 'App' , 'A' , 'B' , 'C' , 'D' ] ) ;
246+
247+ let container = document . createElement ( 'div' ) ;
248+ // We need this to be in the document since we'll dispatch events on it.
249+ document . body . appendChild ( container ) ;
250+
251+ container . innerHTML = finalHTML ;
252+
253+ let spanA = container . getElementsByTagName ( 'span' ) [ 0 ] ;
254+ let spanC = container . getElementsByTagName ( 'span' ) [ 2 ] ;
255+ let spanD = container . getElementsByTagName ( 'span' ) [ 3 ] ;
256+
257+ suspend = true ;
258+
259+ // A and D will be suspended. We'll click on D which should take
260+ // priority, after we unsuspend.
261+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
262+ root . render ( < App /> ) ;
263+
264+ // Nothing has been hydrated so far.
265+ expect ( Scheduler ) . toHaveYielded ( [ ] ) ;
266+
267+ // This click target cannot be hydrated yet because the first is Suspended.
268+ dispatchClickEvent ( spanA ) ;
269+ dispatchClickEvent ( spanC ) ;
270+ dispatchClickEvent ( spanD ) ;
271+
272+ expect ( Scheduler ) . toHaveYielded ( [ 'App' ] ) ;
273+
274+ suspend = false ;
275+ resolve ( ) ;
276+ await promise ;
277+
278+ // We should prioritize hydrating A, C and D first since we clicked in
279+ // them. Only after they're done will we hydrate B.
280+ expect ( Scheduler ) . toFlushAndYield ( [
281+ 'A' ,
282+ 'Clicked A' ,
283+ 'C' ,
284+ 'Clicked C' ,
285+ 'D' ,
286+ 'Clicked D' ,
287+ // B should render last since it wasn't clicked.
288+ 'B' ,
289+ ] ) ;
290+
291+ document . body . removeChild ( container ) ;
292+ } ) ;
117293} ) ;
0 commit comments