9449
);
9450
});
9451
});
9452
+
9453
+ it('does not outline a boundary with suspensey CSS when flushing the shell', async () => {
9454
+ // When flushing the shell, stylesheets with precedence are emitted in the
9455
+ // <head> which blocks paint anyway. So there's no benefit to outlining the
9456
+ // boundary — it would just show a higher-level fallback unnecessarily.
9457
+ // Instead, the boundary should be inlined so the innermost fallback is shown.
9458
+ let streamedContent = '';
9459
+ writable.on('data', chunk => (streamedContent += chunk));
9460
+
9461
+ await act(() => {
9462
+ renderToPipeableStream(
9463
+ <html>
9464
+ <body>
9465
+ <Suspense fallback="Outer Fallback">
9466
+ <Suspense fallback="Middle Fallback">
9467
+ <link rel="stylesheet" href="style.css" precedence="default" />
9468
+ <Suspense fallback="Inner Fallback">
9469
+ <BlockedOn value="content">Async Content</BlockedOn>
9470
+ </Suspense>
9471
+ </Suspense>
9472
+ </Suspense>
9473
+ </body>
9474
+ </html>,
9475
+ ).pipe(writable);
9476
+ });
9477
+
9478
+ // The middle boundary should have been inlined (not outlined) so the
9479
+ // middle fallback text should never appear in the streamed HTML.
9480
+ expect(streamedContent).not.toContain('Middle Fallback');
9481
+
9482
+ // The stylesheet is in the head (blocks paint), and the innermost
9483
+ // fallback is visible.
9484
+ expect(getMeaningfulChildren(document)).toEqual(
9485
+ <html>
9486
+ <head>
9487
+ <link rel="stylesheet" href="style.css" data-precedence="default" />
9488
+ </head>
9489
+ <body>Inner Fallback</body>
9490
+ </html>,
9491
+ );
9492
+
9493
+ // Resolve the async content — streams in without needing to load CSS
9494
+ // since the stylesheet was already in the head.
9495
+ await act(() => {
9496
+ resolveText('content');
9497
+ });
9498
+
9499
+ expect(getMeaningfulChildren(document)).toEqual(
9500
+ <html>
9501
+ <head>
9502
+ <link rel="stylesheet" href="style.css" data-precedence="default" />
9503
+ </head>
9504
+ <body>Async Content</body>
9505
+ </html>,
9506
+ );
9507
+ });
9508
+
9509
+ it('outlines a boundary with suspensey CSS when flushing a streamed completion', async () => {
9510
+ // When a boundary completes via streaming (not as part of the shell),
9511
+ // suspensey CSS should cause the boundary to be outlined. The parent
9512
+ // content can show sooner while the CSS loads separately.
9513
+ let streamedContent = '';
9514
+ writable.on('data', chunk => (streamedContent += chunk));
9515
+
9516
+ await act(() => {
9517
+ renderToPipeableStream(
9518
+ <html>
9519
+ <body>
9520
+ <Suspense fallback="Root Fallback">
9521
+ <BlockedOn value="shell">
9522
+ <Suspense fallback="Outer Fallback">
9523
+ <Suspense fallback="Middle Fallback">
9524
+ <link
9525
+ rel="stylesheet"
9526
+ href="style.css"
9527
+ precedence="default"
9528
+ />
9529
+ <Suspense fallback="Inner Fallback">
9530
+ <BlockedOn value="content">Async Content</BlockedOn>
9531
+ </Suspense>
9532
+ </Suspense>
9533
+ </Suspense>
9534
+ </BlockedOn>
9535
+ </Suspense>
9536
+ </body>
9537
+ </html>,
9538
+ ).pipe(writable);
9539
+ });
9540
+
9541
+ // Shell is showing root fallback
9542
+ expect(getMeaningfulChildren(document)).toEqual(
9543
+ <html>
9544
+ <head />
9545
+ <body>Root Fallback</body>
9546
+ </html>,
9547
+ );
9548
+
9549
+ // Unblock the shell — content streams in. The middle boundary should
9550
+ // be outlined because the CSS arrived via streaming, not in the shell head.
9551
+ streamedContent = '';
9552
+ await act(() => {
9553
+ resolveText('shell');
9554
+ });
9555
+
9556
+ // The middle fallback should appear in the streamed HTML because the
9557
+ // boundary was outlined.
9558
+ expect(streamedContent).toContain('Middle Fallback');
9559
+
9560
+ // The CSS needs to load before the boundary reveals. Until then
9561
+ // the middle fallback is visible.
9562
+ expect(getMeaningfulChildren(document)).toEqual(
9563
+ <html>
9564
+ <head>
9565
+ <link rel="stylesheet" href="style.css" data-precedence="default" />
9566
+ </head>
9567
+ <body>
9568
+ {'Middle Fallback'}
9569
+ <link rel="preload" href="style.css" as="style" />
9570
+ </body>
9571
+ </html>,
9572
+ );
9573
+
9574
+ // Load the stylesheet — now the middle boundary can reveal
9575
+ await act(() => {
9576
+ loadStylesheets();
9577
+ });
9578
+ assertLog(['load stylesheet: style.css']);
9579
+
9580
+ expect(getMeaningfulChildren(document)).toEqual(
9581
+ <html>
9582
+ <head>
9583
+ <link rel="stylesheet" href="style.css" data-precedence="default" />
9584
+ </head>
9585
+ <body>
9586
+ {'Inner Fallback'}
9587
+ <link rel="preload" href="style.css" as="style" />
9588
+ </body>
9589
+ </html>,
9590
+ );
9591
+
9592
+ // Resolve the async content
9593
+ await act(() => {
9594
+ resolveText('content');
9595
+ });
9596
+
9597
+ expect(getMeaningfulChildren(document)).toEqual(
9598
+ <html>
9599
+ <head>
9600
+ <link rel="stylesheet" href="style.css" data-precedence="default" />
9601
+ </head>
9602
+ <body>
9603
+ {'Async Content'}
9604
+ <link rel="preload" href="style.css" as="style" />
9605
+ </body>
9606
+ </html>,
9607
+ );
9608
+ });
9609
+
9610
+ // @gate enableViewTransition
9611
+ it('still outlines a boundary with a suspensey image inside a ViewTransition when flushing the shell', async () => {
9612
+ // Unlike stylesheets (which block paint from the <head> anyway), images
9613
+ // inside ViewTransitions are outlined to enable animation reveals. This
9614
+ // should happen even during the shell flush.
9615
+ const ViewTransition = React.ViewTransition;
9616
+
9617
+ let streamedContent = '';
9618
+ writable.on('data', chunk => (streamedContent += chunk));
9619
+
9620
+ await act(() => {
9621
+ renderToPipeableStream(
9622
+ <html>
9623
+ <body>
9624
+ <ViewTransition>
9625
+ <Suspense fallback="Image Fallback">
9626
+ <link rel="stylesheet" href="style.css" precedence="default" />
9627
+ <img src="large-image.jpg" />
9628
+ <div>Content</div>
9629
+ </Suspense>
9630
+ </ViewTransition>
9631
+ </body>
9632
+ </html>,
9633
+ ).pipe(writable);
9634
+ });
9635
+
9636
+ // The boundary should be outlined because the suspensey image motivates
9637
+ // outlining for animation reveals, even during the shell flush.
9638
+ expect(streamedContent).toContain('Image Fallback');
9639
+ });
9640
});