634
expect(ref.current).toBe(span);
635
expect(container.textContent).toBe('Hi');
636
});
637
+
638
+ it('replaces the fallback with client content if it is not rendered by the server', async () => {
639
+ let suspend = false;
640
+ let promise = new Promise(resolvePromise => {});
641
+ let ref = React.createRef();
642
+
643
+ function Child() {
644
+ if (suspend) {
645
+ throw promise;
646
+ } else {
647
+ return 'Hello';
648
+ }
649
+ }
650
+
651
+ function App() {
652
+ return (
653
+ <div>
654
+ <Suspense fallback="Loading...">
655
+ <span ref={ref}>
656
+ <Child />
657
+ </span>
658
+ </Suspense>
659
+ </div>
660
+ );
661
+ }
662
+
663
+ // First we render the final HTML. With the streaming renderer
664
+ // this may have suspense points on the server but here we want
665
+ // to test the completed HTML. Don't suspend on the server.
666
+ suspend = true;
667
+ let finalHTML = ReactDOMServer.renderToString(<App />);
668
+ let container = document.createElement('div');
669
+ container.innerHTML = finalHTML;
670
+
671
+ expect(container.getElementsByTagName('span').length).toBe(0);
672
+
673
+ // On the client we have the data available quickly for some reason.
674
+ suspend = false;
675
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
676
+ root.render(<App />);
677
+ jest.runAllTimers();
678
+
679
+ expect(container.textContent).toBe('Hello');
680
+
681
+ let span = container.getElementsByTagName('span')[0];
682
+ expect(ref.current).toBe(span);
683
+ });
684
+
685
+ it('waits for pending content to come in from the server and then hydrates it', async () => {
686
+ let suspend = false;
687
+ let promise = new Promise(resolvePromise => {});
688
+ let ref = React.createRef();
689
+
690
+ function Child() {
691
+ if (suspend) {
692
+ throw promise;
693
+ } else {
694
+ return 'Hello';
695
+ }
696
+ }
697
+
698
+ function App() {
699
+ return (
700
+ <div>
701
+ <Suspense fallback="Loading...">
702
+ <span ref={ref}>
703
+ <Child />
704
+ </span>
705
+ </Suspense>
706
+ </div>
707
+ );
708
+ }
709
+
710
+ // We're going to simulate what Fizz will do during streaming rendering.
711
+
712
+ // First we generate the HTML of the loading state.
713
+ suspend = true;
714
+ let loadingHTML = ReactDOMServer.renderToString(<App />);
715
+ // Then we generate the HTML of the final content.
716
+ suspend = false;
717
+ let finalHTML = ReactDOMServer.renderToString(<App />);
718
+
719
+ let container = document.createElement('div');
720
+ container.innerHTML = loadingHTML;
721
+
722
+ let suspenseNode = container.firstChild.firstChild;
723
+ expect(suspenseNode.nodeType).toBe(8);
724
+ // Put the suspense node in hydration state.
725
+ suspenseNode.data = '$?';
726
+
727
+ // This will simulates new content streaming into the document and
728
+ // replacing the fallback with final content.
729
+ function streamInContent() {
730
+ let temp = document.createElement('div');
731
+ temp.innerHTML = finalHTML;
732
+ let finalSuspenseNode = temp.firstChild.firstChild;
733
+ let fallbackContent = suspenseNode.nextSibling;
734
+ let finalContent = finalSuspenseNode.nextSibling;
735
+ suspenseNode.parentNode.replaceChild(finalContent, fallbackContent);
736
+ suspenseNode.data = '$';
737
+ if (suspenseNode._reactRetry) {
738
+ suspenseNode._reactRetry();
739
+ }
740
+ }
741
+
742
+ // We're still showing a fallback.
743
+ expect(container.getElementsByTagName('span').length).toBe(0);
744
+
745
+ // Attempt to hydrate the content.
746
+ suspend = false;
747
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
748
+ root.render(<App />);
749
+ jest.runAllTimers();
750
+
751
+ // We're still loading because we're waiting for the server to stream more content.
752
+ expect(container.textContent).toBe('Loading...');
753
+
754
+ // The server now updates the content in place in the fallback.
755
+ streamInContent();
756
+
757
+ // The final HTML is now in place.
758
+ expect(container.textContent).toBe('Hello');
759
+ let span = container.getElementsByTagName('span')[0];
760
+
761
+ // But it is not yet hydrated.
762
+ expect(ref.current).toBe(null);
763
+
764
+ jest.runAllTimers();
765
+
766
+ // Now it's hydrated.
767
+ expect(ref.current).toBe(span);
768
+ });
769
+
770
+ it('handles an error on the client if the server ends up erroring', async () => {
771
+ let suspend = false;
772
+ let promise = new Promise(resolvePromise => {});
773
+ let ref = React.createRef();
774
+
775
+ function Child() {
776
+ if (suspend) {
777
+ throw promise;
778
+ } else {
779
+ throw new Error('Error Message');
780
+ }
781
+ }
782
+
783
+ class ErrorBoundary extends React.Component {
784
+ state = {error: null};
785
+ static getDerivedStateFromError(error) {
786
+ return {error};
787
+ }
788
+ render() {
789
+ if (this.state.error) {
790
+ return <div ref={ref}>{this.state.error.message}</div>;
791
+ }
792
+ return this.props.children;
793
+ }
794
+ }
795
+
796
+ function App() {
797
+ return (
798
+ <ErrorBoundary>
799
+ <div>
800
+ <Suspense fallback="Loading...">
801
+ <span ref={ref}>
802
+ <Child />
803
+ </span>
804
+ </Suspense>
805
+ </div>
806
+ </ErrorBoundary>
807
+ );
808
+ }
809
+
810
+ // We're going to simulate what Fizz will do during streaming rendering.
811
+
812
+ // First we generate the HTML of the loading state.
813
+ suspend = true;
814
+ let loadingHTML = ReactDOMServer.renderToString(<App />);
815
+
816
+ let container = document.createElement('div');
817
+ container.innerHTML = loadingHTML;
818
+
819
+ let suspenseNode = container.firstChild.firstChild;
820
+ expect(suspenseNode.nodeType).toBe(8);
821
+ // Put the suspense node in hydration state.
822
+ suspenseNode.data = '$?';
823
+
824
+ // This will simulates the server erroring and putting the fallback
825
+ // as the final state.
826
+ function streamInError() {
827
+ suspenseNode.data = '$!';
828
+ if (suspenseNode._reactRetry) {
829
+ suspenseNode._reactRetry();
830
+ }
831
+ }
832
+
833
+ // We're still showing a fallback.
834
+ expect(container.getElementsByTagName('span').length).toBe(0);
835
+
836
+ // Attempt to hydrate the content.
837
+ suspend = false;
838
+ let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
839
+ root.render(<App />);
840
+ jest.runAllTimers();
841
+
842
+ // We're still loading because we're waiting for the server to stream more content.
843
+ expect(container.textContent).toBe('Loading...');
844
+
845
+ // The server now updates the content in place in the fallback.
846
+ streamInError();
847
+
848
+ // The server errored, but we still haven't hydrated. We don't know if the
849
+ // client will succeed yet, so we still show the loading state.
850
+ expect(container.textContent).toBe('Loading...');
851
+ expect(ref.current).toBe(null);
852
+
853
+ jest.runAllTimers();
854
+
855
+ // Hydrating should've generated an error and replaced the suspense boundary.
856
+ expect(container.textContent).toBe('Error Message');
857
+
858
+ let div = container.getElementsByTagName('div')[0];
859
+ expect(ref.current).toBe(div);
860
+ });
861
});