@samitouri / QOS-React / commits / 46ef1ab32a

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;