2346
);
2347
});
2348
2349
+ // @gate enableSuspenseList
2350
+ it('reveals "hidden" rows one by one without suspense boundaries', async () => {
2351
+ const A = createAsyncText('A');
2352
+ const B = createAsyncText('B');
2353
+ const C = createAsyncText('C');
2354
+
2355
+ function Foo() {
2356
+ return (
2357
+ <SuspenseList revealOrder="forwards" tail="hidden">
2358
+ <div>
2359
+ <A />
2360
+ </div>
2361
+ <B />
2362
+ <C />
2363
+ </SuspenseList>
2364
+ );
2365
+ }
2366
+
2367
+ ReactNoop.render(
2368
+ <Suspense fallback="Loading root">
2369
+ <Foo />
2370
+ </Suspense>,
2371
+ );
2372
+
2373
+ await waitForAll(['Suspend! [A]']);
2374
+
2375
+ // We can commit without any rows at all leaving empty.
2376
+ expect(ReactNoop).toMatchRenderedOutput(null);
2377
+
2378
+ await act(() => A.resolve());
2379
+ assertLog(['A', 'Suspend! [B]']);
2380
+
2381
+ expect(ReactNoop).toMatchRenderedOutput(
2382
+ <div>
2383
+ <span>A</span>
2384
+ </div>,
2385
+ );
2386
+
2387
+ await act(() => B.resolve());
2388
+ assertLog(['B', 'Suspend! [C]']);
2389
+
2390
+ // Incremental loading is suspended.
2391
+ jest.advanceTimersByTime(500);
2392
+
2393
+ expect(ReactNoop).toMatchRenderedOutput(
2394
+ <>
2395
+ <div>
2396
+ <span>A</span>
2397
+ </div>
2398
+ <span>B</span>
2399
+ </>,
2400
+ );
2401
+
2402
+ await act(() => C.resolve());
2403
+ assertLog(['C']);
2404
+
2405
+ expect(ReactNoop).toMatchRenderedOutput(
2406
+ <>
2407
+ <div>
2408
+ <span>A</span>
2409
+ </div>
2410
+ <span>B</span>
2411
+ <span>C</span>
2412
+ </>,
2413
+ );
2414
+ });
2415
+
2416
+ // @gate enableSuspenseList
2417
+ it('preserves already mounted rows when a new hidden on is inserted in the tail', async () => {
2418
+ const B = createAsyncText('B');
2419
+ const C = createAsyncText('C');
2420
+
2421
+ let count = 0;
2422
+ function MountCount({children}) {
2423
+ // This component should only mount once.
2424
+ React.useLayoutEffect(() => {
2425
+ count++;
2426
+ }, []);
2427
+ return children;
2428
+ }
2429
+
2430
+ function Foo({insert}) {
2431
+ return (
2432
+ <SuspenseList
2433
+ revealOrder="forwards"
2434
+ tail={insert ? 'hidden' : 'visible'}>
2435
+ <Text text="A" />
2436
+ {insert ? <B /> : null}
2437
+ <MountCount>
2438
+ <Suspense fallback={<Text text="Loading C" />}>
2439
+ <C />
2440
+ </Suspense>
2441
+ </MountCount>
2442
+ </SuspenseList>
2443
+ );
2444
+ }
2445
+
2446
+ await act(() => {
2447
+ ReactNoop.render(<Foo insert={false} />);
2448
+ });
2449
+ assertLog(['A', 'Suspend! [C]', 'Loading C', 'Suspend! [C]']);
2450
+
2451
+ expect(count).toBe(1);
2452
+
2453
+ expect(ReactNoop).toMatchRenderedOutput(
2454
+ <>
2455
+ <span>A</span>
2456
+ <span>Loading C</span>
2457
+ </>,
2458
+ );
2459
+
2460
+ await act(() => {
2461
+ ReactNoop.render(<Foo insert={true} />);
2462
+ });
2463
+
2464
+ assertLog(['A', 'Suspend! [B]', 'A', 'Suspend! [B]']);
2465
+
2466
+ expect(count).toBe(1);
2467
+
2468
+ expect(ReactNoop).toMatchRenderedOutput(
2469
+ <>
2470
+ <span>A</span>
2471
+ <span>Loading C</span>
2472
+ </>,
2473
+ );
2474
+
2475
+ await act(async () => {
2476
+ await B.resolve();
2477
+ await C.resolve();
2478
+ });
2479
+
2480
+ assertLog(['A', 'B', 'C']);
2481
+
2482
+ expect(count).toBe(1);
2483
+
2484
+ expect(ReactNoop).toMatchRenderedOutput(
2485
+ <>
2486
+ <span>A</span>
2487
+ <span>B</span>
2488
+ <span>C</span>
2489
+ </>,
2490
+ );
2491
+ });
2492
+
2493
+ // @gate enableSuspenseList
2494
+ it('reveals "collapsed" rows one by one after the first without boundaries', async () => {
2495
+ const A = createAsyncText('A');
2496
+ const B = createAsyncText('B');
2497
+ const C = createAsyncText('C');
2498
+
2499
+ function Foo() {
2500
+ return (
2501
+ <SuspenseList revealOrder="forwards" tail="collapsed">
2502
+ <A />
2503
+ <Suspense fallback={<Text text="Loading B" />}>
2504
+ <B />
2505
+ </Suspense>
2506
+ <C />
2507
+ </SuspenseList>
2508
+ );
2509
+ }
2510
+
2511
+ await act(async () => {
2512
+ ReactNoop.render(
2513
+ <Suspense fallback="Loading root">
2514
+ <Foo />
2515
+ </Suspense>,
2516
+ );
2517
+ await waitForAll(['Suspend! [A]', 'Suspend! [A]']);
2518
+ });
2519
+
2520
+ // The root is still blocked on the first row.
2521
+ expect(ReactNoop).toMatchRenderedOutput('Loading root');
2522
+
2523
+ await A.resolve();
2524
+
2525
+ await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
2526
+
2527
+ // Incremental loading is suspended.
2528
+ jest.advanceTimersByTime(500);
2529
+
2530
+ // Because we have a Suspense boundary that can commit we can now unblock the rest.
2531
+ // If it wasn't a boundary then we couldn't make progress because it would commit
2532
+ // without any loading state.
2533
+ expect(ReactNoop).toMatchRenderedOutput(
2534
+ <>
2535
+ <span>A</span>
2536
+ <span>Loading B</span>
2537
+ </>,
2538
+ );
2539
+
2540
+ await act(() => B.resolve());
2541
+ assertLog(['B', 'Suspend! [C]', 'B', 'Suspend! [C]']);
2542
+
2543
+ // Incremental loading is suspended.
2544
+ jest.advanceTimersByTime(500);
2545
+
2546
+ // Surprisingly unsuspending B actually causes the parent to resuspend
2547
+ // because C is now unblocked which resuspends the parent. Preventing the
2548
+ // Retry from committing. That's because we don't want to commit into a
2549
+ // state that doesn't have any loading indicators at all. That's what
2550
+ // "collapsed" is for. To ensure there's always a loading indicator.
2551
+ expect(ReactNoop).toMatchRenderedOutput(
2552
+ <>
2553
+ <span>A</span>
2554
+ <span>Loading B</span>
2555
+ </>,
2556
+ );
2557
+
2558
+ await act(() => C.resolve());
2559
+ assertLog(['B', 'C']);
2560
+
2561
+ expect(ReactNoop).toMatchRenderedOutput(
2562
+ <>
2563
+ <span>A</span>
2564
+ <span>B</span>
2565
+ <span>C</span>
2566
+ </>,
2567
+ );
2568
+ });
2569
+
2570
// @gate enableSuspenseList
2571
it('eventually resolves a nested forwards suspense list', async () => {
2572
const B = createAsyncText('B');