@@ -315,6 +315,127 @@ describe('ReactSuspense', () => {
315
315
} ,
316
316
) ;
317
317
318
+ it (
319
+ 'interrupts current render when something suspends with a ' +
320
+ "delay and we've already skipped over a lower priority update in " +
321
+ 'a parent' ,
322
+ ( ) => {
323
+ function interrupt ( ) {
324
+ // React has a heuristic to batch all updates that occur within the same
325
+ // event. This is a trick to circumvent that heuristic.
326
+ ReactTestRenderer . create ( 'whatever' ) ;
327
+ }
328
+
329
+ function App ( { shouldSuspend, step} ) {
330
+ return (
331
+ < >
332
+ < Text text = { `A${ step } ` } />
333
+ < Suspense fallback = { < Text text = "Loading..." /> } >
334
+ { shouldSuspend ? < AsyncText text = "Async" ms = { 2000 } /> : null }
335
+ </ Suspense >
336
+ < Text text = { `B${ step } ` } />
337
+ < Text text = { `C${ step } ` } />
338
+ </ >
339
+ ) ;
340
+ }
341
+
342
+ const root = ReactTestRenderer . create ( null , {
343
+ unstable_isConcurrent : true ,
344
+ } ) ;
345
+
346
+ root . update ( < App shouldSuspend = { false } step = { 0 } /> ) ;
347
+ expect ( Scheduler ) . toFlushAndYield ( [ 'A0' , 'B0' , 'C0' ] ) ;
348
+ expect ( root ) . toMatchRenderedOutput ( 'A0B0C0' ) ;
349
+
350
+ // This update will suspend.
351
+ root . update ( < App shouldSuspend = { true } step = { 1 } /> ) ;
352
+
353
+ // Need to move into the next async bucket.
354
+ Scheduler . unstable_advanceTime ( 1000 ) ;
355
+ // Do a bit of work, then interrupt to trigger a restart.
356
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [ 'A1' ] ) ;
357
+ interrupt ( ) ;
358
+
359
+ // Schedule another update. This will have lower priority because of
360
+ // the interrupt trick above.
361
+ root . update ( < App shouldSuspend = { false } step = { 2 } /> ) ;
362
+
363
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [
364
+ // Should have restarted the first update, because of the interruption
365
+ 'A1' ,
366
+ 'Suspend! [Async]' ,
367
+ 'Loading...' ,
368
+ 'B1' ,
369
+ ] ) ;
370
+
371
+ // Should not have committed loading state
372
+ expect ( root ) . toMatchRenderedOutput ( 'A0B0C0' ) ;
373
+
374
+ // After suspending, should abort the first update and switch to the
375
+ // second update. So, C1 should not appear in the log.
376
+ // TODO: This should work even if React does not yield to the main
377
+ // thread. Should use same mechanism as selective hydration to interrupt
378
+ // the render before the end of the current slice of work.
379
+ expect ( Scheduler ) . toFlushAndYield ( [ 'A2' , 'B2' , 'C2' ] ) ;
380
+
381
+ expect ( root ) . toMatchRenderedOutput ( 'A2B2C2' ) ;
382
+ } ,
383
+ ) ;
384
+
385
+ it (
386
+ 'interrupts current render when something suspends with a ' +
387
+ 'delay, and a parent received an update after it completed' ,
388
+ ( ) => {
389
+ function App ( { shouldSuspend, step} ) {
390
+ return (
391
+ < >
392
+ < Text text = { `A${ step } ` } />
393
+ < Suspense fallback = { < Text text = "Loading..." /> } >
394
+ { shouldSuspend ? < AsyncText text = "Async" ms = { 2000 } /> : null }
395
+ </ Suspense >
396
+ < Text text = { `B${ step } ` } />
397
+ < Text text = { `C${ step } ` } />
398
+ </ >
399
+ ) ;
400
+ }
401
+
402
+ const root = ReactTestRenderer . create ( null , {
403
+ unstable_isConcurrent : true ,
404
+ } ) ;
405
+
406
+ root . update ( < App shouldSuspend = { false } step = { 0 } /> ) ;
407
+ expect ( Scheduler ) . toFlushAndYield ( [ 'A0' , 'B0' , 'C0' ] ) ;
408
+ expect ( root ) . toMatchRenderedOutput ( 'A0B0C0' ) ;
409
+
410
+ // This update will suspend.
411
+ root . update ( < App shouldSuspend = { true } step = { 1 } /> ) ;
412
+ // Flush past the root, but stop before the async component.
413
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [ 'A1' ] ) ;
414
+
415
+ // Schedule an update on the root, which already completed.
416
+ root . update ( < App shouldSuspend = { false } step = { 2 } /> ) ;
417
+ // We'll keep working on the existing update.
418
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [
419
+ // Now the async component suspends
420
+ 'Suspend! [Async]' ,
421
+ 'Loading...' ,
422
+ 'B1' ,
423
+ ] ) ;
424
+
425
+ // Should not have committed loading state
426
+ expect ( root ) . toMatchRenderedOutput ( 'A0B0C0' ) ;
427
+
428
+ // After suspending, should abort the first update and switch to the
429
+ // second update. So, C1 should not appear in the log.
430
+ // TODO: This should work even if React does not yield to the main
431
+ // thread. Should use same mechanism as selective hydration to interrupt
432
+ // the render before the end of the current slice of work.
433
+ expect ( Scheduler ) . toFlushAndYield ( [ 'A2' , 'B2' , 'C2' ] ) ;
434
+
435
+ expect ( root ) . toMatchRenderedOutput ( 'A2B2C2' ) ;
436
+ } ,
437
+ ) ;
438
+
318
439
it ( 'mounts a lazy class component in non-concurrent mode' , async ( ) => {
319
440
class Class extends React . Component {
320
441
componentDidMount ( ) {
0 commit comments