@@ -97,9 +97,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
97
97
textResourceShouldFail = false ;
98
98
} ) ;
99
99
100
- function Text ( props ) {
101
- ReactTestRenderer . unstable_yield ( props . text ) ;
102
- return props . text ;
100
+ function Text ( { fakeRenderDuration = 0 , text = 'Text' } ) {
101
+ advanceTimeBy ( fakeRenderDuration ) ;
102
+ ReactTestRenderer . unstable_yield ( text ) ;
103
+ return text ;
103
104
}
104
105
105
106
function AsyncText ( { fakeRenderDuration = 0 , ms, text} ) {
@@ -250,82 +251,207 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
250
251
return < AsyncText ms = { 1000 } text = "Loaded" fakeRenderDuration = { 1 } /> ;
251
252
} ;
252
253
253
- const NonSuspending = ( ) => {
254
- ReactTestRenderer . unstable_yield ( 'NonSuspending' ) ;
255
- advanceTimeBy ( 5 ) ;
256
- return 'NonSuspending' ;
257
- } ;
258
-
259
- App = ( ) => {
254
+ App = ( { shouldSuspend, text = 'Text' , textRenderDuration = 5 } ) => {
260
255
ReactTestRenderer . unstable_yield ( 'App' ) ;
261
256
return (
262
257
< Profiler id = "root" onRender = { onRender } >
263
258
< Suspense maxDuration = { 500 } fallback = { < Fallback /> } >
264
- < Suspending />
265
- < NonSuspending />
259
+ { shouldSuspend && < Suspending /> }
260
+ < Text fakeRenderDuration = { textRenderDuration } text = { text } />
266
261
</ Suspense >
267
262
</ Profiler >
268
263
) ;
269
264
} ;
270
265
} ) ;
271
266
272
- it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
273
- const root = ReactTestRenderer . create ( < App /> ) ;
274
- expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
275
- expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
267
+ describe ( 'when suspending during mount' , ( ) => {
268
+ it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
269
+ const root = ReactTestRenderer . create ( < App shouldSuspend = { true } /> ) ;
270
+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
271
+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
276
272
277
- // Initial mount only shows the "Loading..." Fallback.
278
- // The treeBaseDuration then should be 10ms spent rendering Fallback,
279
- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
280
- expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
281
- expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
273
+ // Initial mount only shows the "Loading..." Fallback.
274
+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
275
+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
276
+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
277
+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
282
278
283
- jest . advanceTimersByTime ( 1000 ) ;
279
+ jest . advanceTimersByTime ( 1000 ) ;
284
280
285
- expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'NonSuspending ' ] ) ;
286
- expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
281
+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'Text ' ] ) ;
282
+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
287
283
288
- // When the suspending data is resolved and our final UI is rendered,
289
- // the baseDuration should only include the 1ms re-rendering AsyncText,
290
- // but the treeBaseDuration should include the full 8ms spent in the tree.
291
- expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 1 ) ;
292
- expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
284
+ // When the suspending data is resolved and our final UI is rendered,
285
+ // the baseDuration should only include the 1ms re-rendering AsyncText,
286
+ // but the treeBaseDuration should include the full 8ms spent in the tree.
287
+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 1 ) ;
288
+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
289
+ } ) ;
290
+
291
+ it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
292
+ const root = ReactTestRenderer . create ( < App shouldSuspend = { true } /> , {
293
+ unstable_isConcurrent : true ,
294
+ } ) ;
295
+
296
+ expect ( root ) . toFlushAndYield ( [
297
+ 'App' ,
298
+ 'Suspending' ,
299
+ 'Suspend! [Loaded]' ,
300
+ 'Text' ,
301
+ 'Fallback' ,
302
+ ] ) ;
303
+ expect ( root ) . toMatchRenderedOutput ( null ) ;
304
+
305
+ // Show the fallback UI.
306
+ jest . advanceTimersByTime ( 750 ) ;
307
+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
308
+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
309
+
310
+ // Initial mount only shows the "Loading..." Fallback.
311
+ // The treeBaseDuration then should be 10ms spent rendering Fallback,
312
+ // but the actualDuration should also include the 8ms spent rendering the hidden tree.
313
+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
314
+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
315
+
316
+ // Resolve the pending promise.
317
+ jest . advanceTimersByTime ( 250 ) ;
318
+ expect ( ReactTestRenderer ) . toHaveYielded ( [
319
+ 'Promise resolved [Loaded]' ,
320
+ ] ) ;
321
+ expect ( root ) . toFlushAndYield ( [ 'Suspending' , 'Loaded' , 'Text' ] ) ;
322
+ expect ( root ) . toMatchRenderedOutput ( 'LoadedText' ) ;
323
+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
324
+
325
+ // When the suspending data is resolved and our final UI is rendered,
326
+ // both times should include the 8ms re-rendering Suspending and AsyncText.
327
+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 8 ) ;
328
+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
329
+ } ) ;
293
330
} ) ;
294
331
295
- it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
296
- const root = ReactTestRenderer . create ( < App /> , {
297
- unstable_isConcurrent : true ,
332
+ describe ( 'when suspending during update' , ( ) => {
333
+ it ( 'properly accounts for base durations when a suspended times out in a sync tree' , ( ) => {
334
+ const root = ReactTestRenderer . create (
335
+ < App shouldSuspend = { false } textRenderDuration = { 5 } /> ,
336
+ ) ;
337
+ expect ( root . toJSON ( ) ) . toEqual ( 'Text' ) ;
338
+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
339
+
340
+ // Initial mount only shows the "Text" text.
341
+ // It should take 5ms to render.
342
+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 5 ) ;
343
+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 5 ) ;
344
+
345
+ root . update ( < App shouldSuspend = { true } textRenderDuration = { 5 } /> ) ;
346
+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
347
+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
348
+
349
+ // The suspense update should only show the "Loading..." Fallback.
350
+ // Both durations should include 10ms spent rendering Fallback
351
+ // plus the 8ms rendering the (hidden) components.
352
+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 18 ) ;
353
+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 18 ) ;
354
+
355
+ root . update (
356
+ < App shouldSuspend = { true } text = "New" textRenderDuration = { 6 } /> ,
357
+ ) ;
358
+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loading...' ] ) ;
359
+ expect ( onRender ) . toHaveBeenCalledTimes ( 3 ) ;
360
+
361
+ // If we force another update while still timed out,
362
+ // but this time the Text component took 1ms longer to render.
363
+ // This should impact both actualDuration and treeBaseDuration.
364
+ expect ( onRender . mock . calls [ 2 ] [ 2 ] ) . toBe ( 19 ) ;
365
+ expect ( onRender . mock . calls [ 2 ] [ 3 ] ) . toBe ( 19 ) ;
366
+
367
+ jest . advanceTimersByTime ( 1000 ) ;
368
+
369
+ // TODO Change expected onRender count to 4.
370
+ // At the moment, every time we suspended while rendering will cause a commit.
371
+ // This will probably change in the future, but that's why there are two new ones.
372
+ expect ( root . toJSON ( ) ) . toEqual ( [ 'Loaded' , 'New' ] ) ;
373
+ expect ( onRender ) . toHaveBeenCalledTimes ( 5 ) ;
374
+
375
+ // When the suspending data is resolved and our final UI is rendered,
376
+ // the baseDuration should only include the 1ms re-rendering AsyncText,
377
+ // but the treeBaseDuration should include the full 9ms spent in the tree.
378
+ expect ( onRender . mock . calls [ 3 ] [ 2 ] ) . toBe ( 1 ) ;
379
+ expect ( onRender . mock . calls [ 3 ] [ 3 ] ) . toBe ( 9 ) ;
380
+
381
+ // TODO Remove these assertions once this commit is gone.
382
+ // For now, there was no actual work done during this commit; see above comment.
383
+ expect ( onRender . mock . calls [ 4 ] [ 2 ] ) . toBe ( 0 ) ;
384
+ expect ( onRender . mock . calls [ 4 ] [ 3 ] ) . toBe ( 9 ) ;
298
385
} ) ;
299
386
300
- expect ( root ) . toFlushAndYield ( [
301
- 'App' ,
302
- 'Suspending' ,
303
- 'Suspend! [Loaded]' ,
304
- 'NonSuspending' ,
305
- 'Fallback' ,
306
- ] ) ;
307
- expect ( root ) . toMatchRenderedOutput ( null ) ;
308
-
309
- jest . advanceTimersByTime ( 1000 ) ;
310
-
311
- expect ( ReactTestRenderer ) . toHaveYielded ( [ 'Promise resolved [Loaded]' ] ) ;
312
- expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
313
- expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
314
-
315
- // Initial mount only shows the "Loading..." Fallback.
316
- // The treeBaseDuration then should be 10ms spent rendering Fallback,
317
- // but the actualDuration should also include the 8ms spent rendering the hidden tree.
318
- expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 18 ) ;
319
- expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 10 ) ;
320
-
321
- expect ( root ) . toFlushAndYield ( [ 'Suspending' , 'Loaded' , 'NonSuspending' ] ) ;
322
- expect ( root ) . toMatchRenderedOutput ( 'LoadedNonSuspending' ) ;
323
- expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
324
-
325
- // When the suspending data is resolved and our final UI is rendered,
326
- // both times should include the 8ms re-rendering Suspending and AsyncText.
327
- expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 8 ) ;
328
- expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 8 ) ;
387
+ it ( 'properly accounts for base durations when a suspended times out in a concurrent tree' , ( ) => {
388
+ const root = ReactTestRenderer . create (
389
+ < App shouldSuspend = { false } textRenderDuration = { 5 } /> ,
390
+ {
391
+ unstable_isConcurrent : true ,
392
+ } ,
393
+ ) ;
394
+
395
+ expect ( root ) . toFlushAndYield ( [ 'App' , 'Text' ] ) ;
396
+ expect ( root ) . toMatchRenderedOutput ( 'Text' ) ;
397
+ expect ( onRender ) . toHaveBeenCalledTimes ( 1 ) ;
398
+
399
+ // Initial mount only shows the "Text" text.
400
+ // It should take 5ms to render.
401
+ expect ( onRender . mock . calls [ 0 ] [ 2 ] ) . toBe ( 5 ) ;
402
+ expect ( onRender . mock . calls [ 0 ] [ 3 ] ) . toBe ( 5 ) ;
403
+
404
+ root . update ( < App shouldSuspend = { true } textRenderDuration = { 5 } /> ) ;
405
+ expect ( root ) . toFlushAndYield ( [
406
+ 'App' ,
407
+ 'Suspending' ,
408
+ 'Suspend! [Loaded]' ,
409
+ 'Text' ,
410
+ 'Fallback' ,
411
+ ] ) ;
412
+ expect ( root ) . toMatchRenderedOutput ( 'Text' ) ;
413
+
414
+ // Show the fallback UI.
415
+ jest . advanceTimersByTime ( 750 ) ;
416
+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
417
+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
418
+
419
+ // The suspense update should only show the "Loading..." Fallback.
420
+ // The actual duration should include 10ms spent rendering Fallback,
421
+ // plus the 8ms render all of the hidden, suspended subtree.
422
+ // But the tree base duration should only include 10ms spent rendering Fallback,
423
+ // plus the 5ms rendering the previously committed version of the hidden tree.
424
+ expect ( onRender . mock . calls [ 1 ] [ 2 ] ) . toBe ( 18 ) ;
425
+ expect ( onRender . mock . calls [ 1 ] [ 3 ] ) . toBe ( 15 ) ;
426
+
427
+ // Update again while timed out.
428
+ root . update (
429
+ < App shouldSuspend = { true } text = "New" textRenderDuration = { 6 } /> ,
430
+ ) ;
431
+ expect ( root ) . toFlushAndYield ( [
432
+ 'App' ,
433
+ 'Suspending' ,
434
+ 'Suspend! [Loaded]' ,
435
+ 'New' ,
436
+ 'Fallback' ,
437
+ ] ) ;
438
+ expect ( root ) . toMatchRenderedOutput ( 'Loading...' ) ;
439
+ expect ( onRender ) . toHaveBeenCalledTimes ( 2 ) ;
440
+
441
+ // Resolve the pending promise.
442
+ jest . advanceTimersByTime ( 250 ) ;
443
+ expect ( ReactTestRenderer ) . toHaveYielded ( [
444
+ 'Promise resolved [Loaded]' ,
445
+ ] ) ;
446
+ expect ( root ) . toFlushAndYield ( [ 'App' , 'Suspending' , 'Loaded' , 'New' ] ) ;
447
+ expect ( onRender ) . toHaveBeenCalledTimes ( 3 ) ;
448
+
449
+ // When the suspending data is resolved and our final UI is rendered,
450
+ // both times should include the 6ms rendering Text,
451
+ // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
452
+ expect ( onRender . mock . calls [ 2 ] [ 2 ] ) . toBe ( 9 ) ;
453
+ expect ( onRender . mock . calls [ 2 ] [ 3 ] ) . toBe ( 9 ) ;
454
+ } ) ;
329
455
} ) ;
330
456
} ) ;
331
457
} ) ;
0 commit comments