Convert emulated-Fizz PartialHydration tests to Fizz tests (#21437)
Sebastian Markbåge committed
May 5, 2021 at 21:22 UTC
46ef1ab32ad2119ce268b8e292bfa0da7705b852
2 files changed
+173
-275
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+173
@@ -16,6 +16,7 @@ let React;
16
let ReactDOM;
17
let ReactDOMFizzServer;
18
let Suspense;
19
+let SuspenseList;
20
let PropTypes;
21
let textCache;
22
let document;
@@ -37,6 +38,7 @@ describe('ReactDOMFizzServer', () => {
38
}
39
Stream = require('stream');
40
Suspense = React.Suspense;
41
+ SuspenseList = React.unstable_SuspenseList;
42
PropTypes = require('prop-types');
43
44
textCache = new Map();
@@ -476,6 +478,7 @@ describe('ReactDOMFizzServer', () => {
478
});
479
480
// We're still showing a fallback.
481
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
482
483
// Attempt to hydrate the content.
484
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
@@ -507,6 +510,176 @@ describe('ReactDOMFizzServer', () => {
510
expect(ref.current).toBe(h1);
511
});
512
513
+ // @gate experimental
514
+ it('handles an error on the client if the server ends up erroring', async () => {
515
+ const ref = React.createRef();
516
+
517
+ class ErrorBoundary extends React.Component {
518
+ state = {error: null};
519
+ static getDerivedStateFromError(error) {
520
+ return {error};
521
+ }
522
+ render() {
523
+ if (this.state.error) {
524
+ return <b ref={ref}>{this.state.error.message}</b>;
525
+ }
526
+ return this.props.children;
527
+ }
528
+ }
529
+
530
+ function App() {
531
+ return (
532
+ <ErrorBoundary>
533
+ <div>
534
+ <Suspense fallback="Loading...">
535
+ <span ref={ref}>
536
+ <AsyncText text="This Errors" />
537
+ </span>
538
+ </Suspense>
539
+ </div>
540
+ </ErrorBoundary>
541
+ );
542
+ }
543
+
544
+ const loggedErrors = [];
545
+
546
+ // We originally suspend the boundary and start streaming the loading state.
547
+ await act(async () => {
548
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
549
+ <App />,
550
+ writable,
551
+ {
552
+ onError(x) {
553
+ loggedErrors.push(x);
554
+ },
555
+ },
556
+ );
557
+ startWriting();
558
+ });
559
+
560
+ // We're still showing a fallback.
561
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
562
+
563
+ expect(loggedErrors).toEqual([]);
564
+
565
+ // Attempt to hydrate the content.
566
+ const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
567
+ root.render(<App />);
568
+ Scheduler.unstable_flushAll();
569
+
570
+ // We're still loading because we're waiting for the server to stream more content.
571
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
572
+
573
+ const theError = new Error('Error Message');
574
+ await act(async () => {
575
+ rejectText('This Errors', theError);
576
+ });
577
+
578
+ expect(loggedErrors).toEqual([theError]);
579
+
580
+ // The server errored, but we still haven't hydrated. We don't know if the
581
+ // client will succeed yet, so we still show the loading state.
582
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
583
+ expect(ref.current).toBe(null);
584
+
585
+ // Flush the hydration.
586
+ Scheduler.unstable_flushAll();
587
+
588
+ // Hydrating should've generated an error and replaced the suspense boundary.
589
+ expect(getVisibleChildren(container)).toEqual(<b>Error Message</b>);
590
+
591
+ const b = container.getElementsByTagName('b')[0];
592
+ expect(ref.current).toBe(b);
593
+ });
594
+
595
+ // @gate experimental
596
+ it('shows inserted items before pending in a SuspenseList as fallbacks while hydrating', async () => {
597
+ const ref = React.createRef();
598
+
599
+ // These are hoisted to avoid them from rerendering.
600
+ const a = (
601
+ <Suspense fallback="Loading A">
602
+ <span ref={ref}>
603
+ <AsyncText text="A" />
604
+ </span>
605
+ </Suspense>
606
+ );
607
+ const b = (
608
+ <Suspense fallback="Loading B">
609
+ <span>
610
+ <Text text="B" />
611
+ </span>
612
+ </Suspense>
613
+ );
614
+
615
+ function App({showMore}) {
616
+ return (
617
+ <SuspenseList revealOrder="forwards">
618
+ {a}
619
+ {b}
620
+ {showMore ? (
621
+ <Suspense fallback="Loading C">
622
+ <span>C</span>
623
+ </Suspense>
624
+ ) : null}
625
+ </SuspenseList>
626
+ );
627
+ }
628
+
629
+ // We originally suspend the boundary and start streaming the loading state.
630
+ await act(async () => {
631
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
632
+ <App showMore={false} />,
633
+ writable,
634
+ );
635
+ startWriting();
636
+ });
637
+
638
+ const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
639
+ root.render(<App showMore={false} />);
640
+ Scheduler.unstable_flushAll();
641
+
642
+ // We're not hydrated yet.
643
+ expect(ref.current).toBe(null);
644
+ expect(getVisibleChildren(container)).toEqual([
645
+ 'Loading A',
646
+ // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
647
+ // isn't implemented fully yet.
648
+ <span>B</span>,
649
+ ]);
650
+
651
+ // Add more rows before we've hydrated the first two.
652
+ root.render(<App showMore={true} />);
653
+ Scheduler.unstable_flushAll();
654
+
655
+ // We're not hydrated yet.
656
+ expect(ref.current).toBe(null);
657
+
658
+ // We haven't resolved yet.
659
+ expect(getVisibleChildren(container)).toEqual([
660
+ 'Loading A',
661
+ // TODO: This is incorrect. It should be "Loading B" but Fizz SuspenseList
662
+ // isn't implemented fully yet.
663
+ <span>B</span>,
664
+ 'Loading C',
665
+ ]);
666
+
667
+ await act(async () => {
668
+ await resolveText('A');
669
+ });
670
+
671
+ Scheduler.unstable_flushAll();
672
+
673
+ expect(getVisibleChildren(container)).toEqual([
674
+ <span>A</span>,
675
+ <span>B</span>,
676
+ <span>C</span>,
677
+ ]);
678
+
679
+ const span = container.getElementsByTagName('span')[0];
680
+ expect(ref.current).toBe(span);
681
+ });
682
+
683
// @gate experimental
684
it('client renders a boundary if it does not resolve before aborting', async () => {
685
function App() {
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
-275
@@ -1358,189 +1358,6 @@ describe('ReactDOMServerPartialHydration', () => {
1358
expect(ref.current).toBe(span);
1359
});
1360
1361
- // @gate experimental
1362
- it('waits for pending content to come in from the server and then hydrates it', async () => {
1363
- let suspend = false;
1364
- const promise = new Promise(resolvePromise => {});
1365
- const ref = React.createRef();
1366
-
1367
- function Child() {
1368
- if (suspend) {
1369
- throw promise;
1370
- } else {
1371
- return 'Hello';
1372
- }
1373
- }
1374
-
1375
- function App() {
1376
- return (
1377
- <div>
1378
- <Suspense fallback="Loading...">
1379
- <span ref={ref}>
1380
- <Child />
1381
- </span>
1382
- </Suspense>
1383
- </div>
1384
- );
1385
- }
1386
-
1387
- // We're going to simulate what Fizz will do during streaming rendering.
1388
-
1389
- // First we generate the HTML of the loading state.
1390
- suspend = true;
1391
- const loadingHTML = ReactDOMServer.renderToString(<App />);
1392
- // Then we generate the HTML of the final content.
1393
- suspend = false;
1394
- const finalHTML = ReactDOMServer.renderToString(<App />);
1395
-
1396
- const container = document.createElement('div');
1397
- container.innerHTML = loadingHTML;
1398
-
1399
- const suspenseNode = container.firstChild.firstChild;
1400
- expect(suspenseNode.nodeType).toBe(8);
1401
- // Put the suspense node in hydration state.
1402
- suspenseNode.data = '$?';
1403
-
1404
- // This will simulates new content streaming into the document and
1405
- // replacing the fallback with final content.
1406
- function streamInContent() {
1407
- const temp = document.createElement('div');
1408
- temp.innerHTML = finalHTML;
1409
- const finalSuspenseNode = temp.firstChild.firstChild;
1410
- const fallbackContent = suspenseNode.nextSibling;
1411
- const finalContent = finalSuspenseNode.nextSibling;
1412
- suspenseNode.parentNode.replaceChild(finalContent, fallbackContent);
1413
- suspenseNode.data = '$';
1414
- if (suspenseNode._reactRetry) {
1415
- suspenseNode._reactRetry();
1416
- }
1417
- }
1418
-
1419
- // We're still showing a fallback.
1420
- expect(container.getElementsByTagName('span').length).toBe(0);
1421
-
1422
- // Attempt to hydrate the content.
1423
- suspend = false;
1424
- const root = ReactDOM.createRoot(container, {hydrate: true});
1425
- root.render(<App />);
1426
- Scheduler.unstable_flushAll();
1427
- jest.runAllTimers();
1428
-
1429
- // We're still loading because we're waiting for the server to stream more content.
1430
- expect(container.textContent).toBe('Loading...');
1431
-
1432
- // The server now updates the content in place in the fallback.
1433
- streamInContent();
1434
-
1435
- // The final HTML is now in place.
1436
- expect(container.textContent).toBe('Hello');
1437
- const span = container.getElementsByTagName('span')[0];
1438
-
1439
- // But it is not yet hydrated.
1440
- expect(ref.current).toBe(null);
1441
-
1442
- Scheduler.unstable_flushAll();
1443
- jest.runAllTimers();
1444
-
1445
- // Now it's hydrated.
1446
- expect(ref.current).toBe(span);
1447
- });
1448
-
1449
- // @gate experimental
1450
- it('handles an error on the client if the server ends up erroring', async () => {
1451
- let suspend = false;
1452
- const promise = new Promise(resolvePromise => {});
1453
- const ref = React.createRef();
1454
-
1455
- function Child() {
1456
- if (suspend) {
1457
- throw promise;
1458
- } else {
1459
- throw new Error('Error Message');
1460
- }
1461
- }
1462
-
1463
- class ErrorBoundary extends React.Component {
1464
- state = {error: null};
1465
- static getDerivedStateFromError(error) {
1466
- return {error};
1467
- }
1468
- render() {
1469
- if (this.state.error) {
1470
- return <div ref={ref}>{this.state.error.message}</div>;
1471
- }
1472
- return this.props.children;
1473
- }
1474
- }
1475
-
1476
- function App() {
1477
- return (
1478
- <ErrorBoundary>
1479
- <div>
1480
- <Suspense fallback="Loading...">
1481
- <span ref={ref}>
1482
- <Child />
1483
- </span>
1484
- </Suspense>
1485
- </div>
1486
- </ErrorBoundary>
1487
- );
1488
- }
1489
-
1490
- // We're going to simulate what Fizz will do during streaming rendering.
1491
-
1492
- // First we generate the HTML of the loading state.
1493
- suspend = true;
1494
- const loadingHTML = ReactDOMServer.renderToString(<App />);
1495
-
1496
- const container = document.createElement('div');
1497
- container.innerHTML = loadingHTML;
1498
-
1499
- const suspenseNode = container.firstChild.firstChild;
1500
- expect(suspenseNode.nodeType).toBe(8);
1501
- // Put the suspense node in hydration state.
1502
- suspenseNode.data = '$?';
1503
-
1504
- // This will simulates the server erroring and putting the fallback
1505
- // as the final state.
1506
- function streamInError() {
1507
- suspenseNode.data = '$!';
1508
- if (suspenseNode._reactRetry) {
1509
- suspenseNode._reactRetry();
1510
- }
1511
- }
1512
-
1513
- // We're still showing a fallback.
1514
- expect(container.getElementsByTagName('span').length).toBe(0);
1515
-
1516
- // Attempt to hydrate the content.
1517
- suspend = false;
1518
- const root = ReactDOM.createRoot(container, {hydrate: true});
1519
- root.render(<App />);
1520
- Scheduler.unstable_flushAll();
1521
- jest.runAllTimers();
1522
-
1523
- // We're still loading because we're waiting for the server to stream more content.
1524
- expect(container.textContent).toBe('Loading...');
1525
-
1526
- // The server now updates the content in place in the fallback.
1527
- streamInError();
1528
-
1529
- // The server errored, but we still haven't hydrated. We don't know if the
1530
- // client will succeed yet, so we still show the loading state.
1531
- expect(container.textContent).toBe('Loading...');
1532
- expect(ref.current).toBe(null);
1533
-
1534
- Scheduler.unstable_flushAll();
1535
- jest.runAllTimers();
1536
-
1537
- // Hydrating should've generated an error and replaced the suspense boundary.
1538
- expect(container.textContent).toBe('Error Message');
1539
-
1540
- const div = container.getElementsByTagName('div')[0];
1541
- expect(ref.current).toBe(div);
1542
- });
1543
-
1361
// @gate experimental
1362
it('shows inserted items in a SuspenseList before content is hydrated', async () => {
1363
let suspend = false;
@@ -1702,98 +1519,6 @@ describe('ReactDOMServerPartialHydration', () => {
1519
expect(container.textContent).toBe('ALoading B');
1520
});
1521
1705
- // @gate experimental
1706
- it('shows inserted items before pending in a SuspenseList as fallbacks', async () => {
1707
- let suspend = false;
1708
- let resolve;
1709
- const promise = new Promise(resolvePromise => (resolve = resolvePromise));
1710
- const ref = React.createRef();
1711
-
1712
- function Child({children}) {
1713
- if (suspend) {
1714
- throw promise;
1715
- } else {
1716
- return children;
1717
- }
1718
- }
1719
-
1720
- // These are hoisted to avoid them from rerendering.
1721
- const a = (
1722
- <Suspense fallback="Loading A">
1723
- <Child>
1724
- <span>A</span>
1725
- </Child>
1726
- </Suspense>
1727
- );
1728
- const b = (
1729
- <Suspense fallback="Loading B">
1730
- <Child>
1731
- <span ref={ref}>B</span>
1732
- </Child>
1733
- </Suspense>
1734
- );
1735
-
1736
- function App({showMore}) {
1737
- return (
1738
- <SuspenseList revealOrder="forwards">
1739
- {a}
1740
- {b}
1741
- {showMore ? (
1742
- <Suspense fallback="Loading C">
1743
- <span>C</span>
1744
- </Suspense>
1745
- ) : null}
1746
- </SuspenseList>
1747
- );
1748
- }
1749
-
1750
- suspend = false;
1751
- const html = ReactDOMServer.renderToString(<App showMore={false} />);
1752
-
1753
- const container = document.createElement('div');
1754
- container.innerHTML = html;
1755
-
1756
- const suspenseNode = container.firstChild;
1757
- expect(suspenseNode.nodeType).toBe(8);
1758
- // Put the suspense node in pending state.
1759
- suspenseNode.data = '$?';
1760
-
1761
- const root = ReactDOM.createRoot(container, {hydrate: true});
1762
-
1763
- suspend = true;
1764
- act(() => {
1765
- root.render(<App showMore={false} />);
1766
- });
1767
-
1768
- // We're not hydrated yet.
1769
- expect(ref.current).toBe(null);
1770
- expect(container.textContent).toBe('AB');
1771
-
1772
- // Add more rows before we've hydrated the first two.
1773
- act(() => {
1774
- root.render(<App showMore={true} />);
1775
- });
1776
-
1777
- // We're not hydrated yet.
1778
- expect(ref.current).toBe(null);
1779
-
1780
- // Since the first two are already showing their final content
1781
- // we should be able to show the real content.
1782
- expect(container.textContent).toBe('ABLoading C');
1783
-
1784
- suspend = false;
1785
- await act(async () => {
1786
- // Resolve the boundary to be in its resolved final state.
1787
- suspenseNode.data = '$';
1788
- if (suspenseNode._reactRetry) {
1789
- suspenseNode._reactRetry();
1790
- }
1791
- await resolve();
1792
- });
1793
-
1794
- expect(container.textContent).toBe('ABC');
1795
- });
1796
-
1522
// @gate experimental
1523
it('clears server boundaries when SuspenseList runs out of time hydrating', async () => {
1524
let suspend = false;