234
return readText(text);
235
}
236
237
+ function AsyncTextWrapped({as, text}) {
238
+ const As = as;
239
+ return <As>{readText(text)}</As>;
240
+ }
241
+
242
// @gate experimental
243
it('should asynchronously load a lazy component', async () => {
244
let resolveA;
3582
</div>,
3583
);
3584
});
3585
+
3586
+ describe('text separators', () => {
3587
+ // To force performWork to start before resolving AsyncText but before piping we need to wait until
3588
+ // after scheduleWork which currently uses setImmediate to delay performWork
3589
+ function afterImmediate() {
3590
+ return new Promise(resolve => {
3591
+ setImmediate(resolve);
3592
+ });
3593
+ }
3594
+
3595
+ // @gate experimental
3596
+ it('it only includes separators between adjacent text nodes', async () => {
3597
+ function App({name}) {
3598
+ return (
3599
+ <div>
3600
+ hello<b>world, {name}</b>!
3601
+ </div>
3602
+ );
3603
+ }
3604
+
3605
+ await act(async () => {
3606
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3607
+ <App name="Foo" />,
3608
+ );
3609
+ pipe(writable);
3610
+ });
3611
+
3612
+ expect(container.innerHTML).toEqual(
3613
+ '<div>hello<b>world, <!-- -->Foo</b>!</div>',
3614
+ );
3615
+ const errors = [];
3616
+ ReactDOMClient.hydrateRoot(container, <App name="Foo" />, {
3617
+ onRecoverableError(error) {
3618
+ errors.push(error.message);
3619
+ },
3620
+ });
3621
+ expect(Scheduler).toFlushAndYield([]);
3622
+ expect(errors).toEqual([]);
3623
+ expect(getVisibleChildren(container)).toEqual(
3624
+ <div>
3625
+ hello<b>world, {'Foo'}</b>!
3626
+ </div>,
3627
+ );
3628
+ });
3629
+
3630
+ // @gate experimental
3631
+ it('it does not insert text separators even when adjacent text is in a delayed segment', async () => {
3632
+ function App({name}) {
3633
+ return (
3634
+ <Suspense fallback={'loading...'}>
3635
+ <div id="app-div">
3636
+ hello
3637
+ <b>
3638
+ world, <AsyncText text={name} />
3639
+ </b>
3640
+ !
3641
+ </div>
3642
+ </Suspense>
3643
+ );
3644
+ }
3645
+
3646
+ await act(async () => {
3647
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3648
+ <App name="Foo" />,
3649
+ );
3650
+ pipe(writable);
3651
+ });
3652
+
3653
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3654
+ '<div id="app-div">hello<b>world, <template id="P:1"></template></b>!</div>',
3655
+ );
3656
+
3657
+ await act(() => resolveText('Foo'));
3658
+
3659
+ expect(container.firstElementChild.outerHTML).toEqual(
3660
+ '<div id="app-div">hello<b>world, Foo</b>!</div>',
3661
+ );
3662
+ // there are extra script nodes at the end of container
3663
+ expect(container.childNodes.length).toBe(5);
3664
+ const div = container.childNodes[1];
3665
+ expect(div.childNodes.length).toBe(3);
3666
+ const b = div.childNodes[1];
3667
+ expect(b.childNodes.length).toBe(2);
3668
+ expect(b.childNodes[0]).toMatchInlineSnapshot('world, ');
3669
+ expect(b.childNodes[1]).toMatchInlineSnapshot('Foo');
3670
+
3671
+ const errors = [];
3672
+ ReactDOMClient.hydrateRoot(container, <App name="Foo" />, {
3673
+ onRecoverableError(error) {
3674
+ errors.push(error.message);
3675
+ },
3676
+ });
3677
+ expect(Scheduler).toFlushAndYield([]);
3678
+ expect(errors).toEqual([]);
3679
+ expect(getVisibleChildren(container)).toEqual(
3680
+ <div id="app-div">
3681
+ hello<b>world, {'Foo'}</b>!
3682
+ </div>,
3683
+ );
3684
+ });
3685
+
3686
+ // @gate experimental
3687
+ it('it works with multiple adjacent segments', async () => {
3688
+ function App() {
3689
+ return (
3690
+ <Suspense fallback={'loading...'}>
3691
+ <div id="app-div">
3692
+ h<AsyncText text={'ello'} />
3693
+ w<AsyncText text={'orld'} />
3694
+ </div>
3695
+ </Suspense>
3696
+ );
3697
+ }
3698
+
3699
+ await act(async () => {
3700
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3701
+ pipe(writable);
3702
+ });
3703
+
3704
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3705
+ '<div id="app-div">h<template id="P:1"></template>w<template id="P:2"></template></div>',
3706
+ );
3707
+
3708
+ await act(() => resolveText('orld'));
3709
+
3710
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3711
+ '<div id="app-div">h<template id="P:1"></template>world</div>',
3712
+ );
3713
+
3714
+ await act(() => resolveText('ello'));
3715
+ expect(container.firstElementChild.outerHTML).toEqual(
3716
+ '<div id="app-div">helloworld</div>',
3717
+ );
3718
+
3719
+ const errors = [];
3720
+ ReactDOMClient.hydrateRoot(container, <App name="Foo" />, {
3721
+ onRecoverableError(error) {
3722
+ errors.push(error.message);
3723
+ },
3724
+ });
3725
+ expect(Scheduler).toFlushAndYield([]);
3726
+ expect(errors).toEqual([]);
3727
+ expect(getVisibleChildren(container)).toEqual(
3728
+ <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>,
3729
+ );
3730
+ });
3731
+
3732
+ // @gate experimental
3733
+ it('it works when some segments are flushed and others are patched', async () => {
3734
+ function App() {
3735
+ return (
3736
+ <Suspense fallback={'loading...'}>
3737
+ <div id="app-div">
3738
+ h<AsyncText text={'ello'} />
3739
+ w<AsyncText text={'orld'} />
3740
+ </div>
3741
+ </Suspense>
3742
+ );
3743
+ }
3744
+
3745
+ await act(async () => {
3746
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3747
+ await afterImmediate();
3748
+ await act(() => resolveText('ello'));
3749
+ pipe(writable);
3750
+ });
3751
+
3752
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3753
+ '<div id="app-div">h<!-- -->ello<!-- -->w<template id="P:1"></template></div>',
3754
+ );
3755
+
3756
+ await act(() => resolveText('orld'));
3757
+
3758
+ expect(container.firstElementChild.outerHTML).toEqual(
3759
+ '<div id="app-div">h<!-- -->ello<!-- -->world</div>',
3760
+ );
3761
+
3762
+ const errors = [];
3763
+ ReactDOMClient.hydrateRoot(container, <App />, {
3764
+ onRecoverableError(error) {
3765
+ errors.push(error.message);
3766
+ },
3767
+ });
3768
+ expect(Scheduler).toFlushAndYield([]);
3769
+ expect(errors).toEqual([]);
3770
+ expect(getVisibleChildren(container)).toEqual(
3771
+ <div id="app-div">{['h', 'ello', 'w', 'orld']}</div>,
3772
+ );
3773
+ });
3774
+
3775
+ // @gate experimental
3776
+ it('it does not prepend a text separators if the segment follows a non-Text Node', async () => {
3777
+ function App() {
3778
+ return (
3779
+ <Suspense fallback={'loading...'}>
3780
+ <div>
3781
+ hello
3782
+ <b>
3783
+ <AsyncText text={'world'} />
3784
+ </b>
3785
+ </div>
3786
+ </Suspense>
3787
+ );
3788
+ }
3789
+
3790
+ await act(async () => {
3791
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3792
+ await afterImmediate();
3793
+ await act(() => resolveText('world'));
3794
+ pipe(writable);
3795
+ });
3796
+
3797
+ expect(container.firstElementChild.outerHTML).toEqual(
3798
+ '<div>hello<b>world<!-- --></b></div>',
3799
+ );
3800
+
3801
+ const errors = [];
3802
+ ReactDOMClient.hydrateRoot(container, <App />, {
3803
+ onRecoverableError(error) {
3804
+ errors.push(error.message);
3805
+ },
3806
+ });
3807
+ expect(Scheduler).toFlushAndYield([]);
3808
+ expect(errors).toEqual([]);
3809
+ expect(getVisibleChildren(container)).toEqual(
3810
+ <div>
3811
+ hello<b>world</b>
3812
+ </div>,
3813
+ );
3814
+ });
3815
+
3816
+ // @gate experimental
3817
+ it('it does not prepend a text separators if the segments first emission is a non-Text Node', async () => {
3818
+ function App() {
3819
+ return (
3820
+ <Suspense fallback={'loading...'}>
3821
+ <div>
3822
+ hello
3823
+ <AsyncTextWrapped as={'b'} text={'world'} />
3824
+ </div>
3825
+ </Suspense>
3826
+ );
3827
+ }
3828
+
3829
+ await act(async () => {
3830
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3831
+ await afterImmediate();
3832
+ await act(() => resolveText('world'));
3833
+ pipe(writable);
3834
+ });
3835
+
3836
+ expect(container.firstElementChild.outerHTML).toEqual(
3837
+ '<div>hello<b>world</b></div>',
3838
+ );
3839
+
3840
+ const errors = [];
3841
+ ReactDOMClient.hydrateRoot(container, <App />, {
3842
+ onRecoverableError(error) {
3843
+ errors.push(error.message);
3844
+ },
3845
+ });
3846
+ expect(Scheduler).toFlushAndYield([]);
3847
+ expect(errors).toEqual([]);
3848
+ expect(getVisibleChildren(container)).toEqual(
3849
+ <div>
3850
+ hello<b>world</b>
3851
+ </div>,
3852
+ );
3853
+ });
3854
+
3855
+ // @gate experimental
3856
+ it('should not insert separators for text inside Suspense boundaries even if they would otherwise be considered text-embedded', async () => {
3857
+ function App() {
3858
+ return (
3859
+ <Suspense fallback={'loading...'}>
3860
+ <div id="app-div">
3861
+ start
3862
+ <Suspense fallback={'[loading first]'}>
3863
+ firststart
3864
+ <AsyncText text={'first suspended'} />
3865
+ firstend
3866
+ </Suspense>
3867
+ <Suspense fallback={'[loading second]'}>
3868
+ secondstart
3869
+ <b>
3870
+ <AsyncText text={'second suspended'} />
3871
+ </b>
3872
+ </Suspense>
3873
+ end
3874
+ </div>
3875
+ </Suspense>
3876
+ );
3877
+ }
3878
+
3879
+ await act(async () => {
3880
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3881
+ await afterImmediate();
3882
+ await act(() => resolveText('world'));
3883
+ pipe(writable);
3884
+ });
3885
+
3886
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3887
+ '<div id="app-div">start<!--$?--><template id="B:0"></template>[loading first]<!--/$--><!--$?--><template id="B:1"></template>[loading second]<!--/$-->end</div>',
3888
+ );
3889
+
3890
+ await act(async () => {
3891
+ resolveText('first suspended');
3892
+ });
3893
+
3894
+ expect(document.getElementById('app-div').outerHTML).toEqual(
3895
+ '<div id="app-div">start<!--$-->firststartfirst suspendedfirstend<!--/$--><!--$?--><template id="B:1"></template>[loading second]<!--/$-->end</div>',
3896
+ );
3897
+
3898
+ const errors = [];
3899
+ ReactDOMClient.hydrateRoot(container, <App />, {
3900
+ onRecoverableError(error) {
3901
+ errors.push(error.message);
3902
+ },
3903
+ });
3904
+ expect(Scheduler).toFlushAndYield([]);
3905
+ expect(errors).toEqual([]);
3906
+ expect(getVisibleChildren(container)).toEqual(
3907
+ <div id="app-div">
3908
+ {'start'}
3909
+ {'firststart'}
3910
+ {'first suspended'}
3911
+ {'firstend'}
3912
+ {'[loading second]'}
3913
+ {'end'}
3914
+ </div>,
3915
+ );
3916
+
3917
+ await act(async () => {
3918
+ resolveText('second suspended');
3919
+ });
3920
+
3921
+ expect(container.firstElementChild.outerHTML).toEqual(
3922
+ '<div id="app-div">start<!--$-->firststartfirst suspendedfirstend<!--/$--><!--$-->secondstart<b>second suspended</b><!--/$-->end</div>',
3923
+ );
3924
+
3925
+ expect(Scheduler).toFlushAndYield([]);
3926
+ expect(errors).toEqual([]);
3927
+ expect(getVisibleChildren(container)).toEqual(
3928
+ <div id="app-div">
3929
+ {'start'}
3930
+ {'firststart'}
3931
+ {'first suspended'}
3932
+ {'firstend'}
3933
+ {'secondstart'}
3934
+ <b>second suspended</b>
3935
+ {'end'}
3936
+ </div>,
3937
+ );
3938
+ });
3939
+
3940
+ // @gate experimental
3941
+ it('(only) includes extraneous text separators in segments that complete before flushing, followed by nothing or a non-Text node', async () => {
3942
+ function App() {
3943
+ return (
3944
+ <div>
3945
+ <Suspense fallback={'text before, nothing after...'}>
3946
+ hello
3947
+ <AsyncText text="world" />
3948
+ </Suspense>
3949
+ <Suspense fallback={'nothing before or after...'}>
3950
+ <AsyncText text="world" />
3951
+ </Suspense>
3952
+ <Suspense fallback={'text before, element after...'}>
3953
+ hello
3954
+ <AsyncText text="world" />
3955
+ <br />
3956
+ </Suspense>
3957
+ <Suspense fallback={'nothing before, element after...'}>
3958
+ <AsyncText text="world" />
3959
+ <br />
3960
+ </Suspense>
3961
+ </div>
3962
+ );
3963
+ }
3964
+
3965
+ await act(async () => {
3966
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
3967
+ await afterImmediate();
3968
+ await act(() => resolveText('world'));
3969
+ pipe(writable);
3970
+ });
3971
+
3972
+ expect(container.innerHTML).toEqual(
3973
+ '<div><!--$-->hello<!-- -->world<!-- --><!--/$--><!--$-->world<!-- --><!--/$--><!--$-->hello<!-- -->world<!-- --><br><!--/$--><!--$-->world<!-- --><br><!--/$--></div>',
3974
+ );
3975
+
3976
+ const errors = [];
3977
+ ReactDOMClient.hydrateRoot(container, <App />, {
3978
+ onRecoverableError(error) {
3979
+ errors.push(error.message);
3980
+ },
3981
+ });
3982
+ expect(Scheduler).toFlushAndYield([]);
3983
+ expect(errors).toEqual([]);
3984
+ expect(getVisibleChildren(container)).toEqual(
3985
+ <div>
3986
+ {/* first boundary */}
3987
+ {'hello'}
3988
+ {'world'}
3989
+ {/* second boundary */}
3990
+ {'world'}
3991
+ {/* third boundary */}
3992
+ {'hello'}
3993
+ {'world'}
3994
+ <br />
3995
+ {/* fourth boundary */}
3996
+ {'world'}
3997
+ <br />
3998
+ </div>,
3999
+ );
4000
+ });
4001
+ });
4002
});