726
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
727
});
728
729
- // TODO: This cannot be tested until we have a way to long-suspend navigations.
730
- it.skip('starts working on an update even if its priority falls between two suspended levels', async () => {
729
+ it('starts working on an update even if its priority falls between two suspended levels', async () => {
730
function App(props) {
731
return (
732
<Suspense fallback={<Text text="Loading..." />}>
734
- {props.text === 'C' ? (
735
- <Text text="C" />
733
+ {props.text === 'C' || props.text === 'S' ? (
734
+ <Text text={props.text} />
735
) : (
736
<AsyncText text={props.text} ms={10000} />
737
)}
739
);
740
}
741
743
- // Schedule an update
744
- ReactNoop.render(<App text="A" />);
742
+ // First mount without suspending. This ensures we already have content
743
+ // showing so that subsequent updates will suspend.
744
+ ReactNoop.render(<App text="S" />);
745
+ expect(Scheduler).toFlushAndYield(['S']);
746
+
747
+ // Schedule an update, and suspend for up to 5 seconds.
748
+ React.unstable_withSuspenseConfig(
749
+ () => ReactNoop.render(<App text="A" />),
750
+ {
751
+ timeoutMs: 5000,
752
+ },
753
+ );
754
// The update should suspend.
755
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
747
- expect(ReactNoop.getChildren()).toEqual([]);
756
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
757
749
- // Advance time until right before it expires. This number may need to
750
- // change if the default expiration for low priority updates is adjusted.
758
+ // Advance time until right before it expires.
759
await advanceTimers(4999);
760
ReactNoop.expire(4999);
761
expect(Scheduler).toFlushWithoutYielding();
754
- expect(ReactNoop.getChildren()).toEqual([]);
762
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
763
764
// Schedule another low priority update.
757
- ReactNoop.render(<App text="B" />);
765
+ React.unstable_withSuspenseConfig(
766
+ () => ReactNoop.render(<App text="B" />),
767
+ {
768
+ timeoutMs: 10000,
769
+ },
770
+ );
771
// This update should also suspend.
772
expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
760
- expect(ReactNoop.getChildren()).toEqual([]);
773
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
774
762
- // Schedule a high priority update. Its expiration time will fall between
775
+ // Schedule a regular update. Its expiration time will fall between
776
// the expiration times of the previous two updates.
764
- ReactNoop.interactiveUpdates(() => {
765
- ReactNoop.render(<App text="C" />);
766
- });
777
+ ReactNoop.render(<App text="C" />);
778
expect(Scheduler).toFlushAndYield(['C']);
779
expect(ReactNoop.getChildren()).toEqual([span('C')]);
780
1671
ReactNoop.render(<Foo />);
1672
1673
// Took a long time to render. This is to ensure we get a long suspense time.
1663
- // Could also use something like suspendIfNeeded to simulate this.
1674
+ // Could also use something like withSuspenseConfig to simulate this.
1675
Scheduler.advanceTime(1500);
1676
await advanceTimers(1500);
1677
1701
1702
expect(ReactNoop.getChildren()).toEqual([span('Loading A...')]);
1703
});
1704
+
1705
+ describe('delays transitions when there a suspense config is supplied', () => {
1706
+ const SUSPENSE_CONFIG = {
1707
+ timeoutMs: 2000,
1708
+ };
1709
+
1710
+ it('top level render', async () => {
1711
+ function App({page}) {
1712
+ return (
1713
+ <Suspense fallback={<Text text="Loading..." />}>
1714
+ <AsyncText text={page} ms={5000} />
1715
+ </Suspense>
1716
+ );
1717
+ }
1718
+
1719
+ // Initial render.
1720
+ React.unstable_withSuspenseConfig(
1721
+ () => ReactNoop.render(<App page="A" />),
1722
+ SUSPENSE_CONFIG,
1723
+ );
1724
+
1725
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
1726
+ // Only a short time is needed to unsuspend the initial loading state.
1727
+ Scheduler.advanceTime(400);
1728
+ await advanceTimers(400);
1729
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1730
+
1731
+ // Later we load the data.
1732
+ Scheduler.advanceTime(5000);
1733
+ await advanceTimers(5000);
1734
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1735
+ expect(Scheduler).toFlushAndYield(['A']);
1736
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1737
+
1738
+ // Start transition.
1739
+ React.unstable_withSuspenseConfig(
1740
+ () => ReactNoop.render(<App page="B" />),
1741
+ SUSPENSE_CONFIG,
1742
+ );
1743
+
1744
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
1745
+ Scheduler.advanceTime(1000);
1746
+ await advanceTimers(1000);
1747
+ // Even after a second, we have still not yet flushed the loading state.
1748
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1749
+ Scheduler.advanceTime(1100);
1750
+ await advanceTimers(1100);
1751
+ // After the timeout, we do show the loading state.
1752
+ expect(ReactNoop.getChildren()).toEqual([
1753
+ hiddenSpan('A'),
1754
+ span('Loading...'),
1755
+ ]);
1756
+ // Later we load the data.
1757
+ Scheduler.advanceTime(3000);
1758
+ await advanceTimers(3000);
1759
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1760
+ expect(Scheduler).toFlushAndYield(['B']);
1761
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1762
+ });
1763
+
1764
+ it('hooks', async () => {
1765
+ let transitionToPage;
1766
+ function App() {
1767
+ let [page, setPage] = React.useState('none');
1768
+ transitionToPage = setPage;
1769
+ if (page === 'none') {
1770
+ return null;
1771
+ }
1772
+ return (
1773
+ <Suspense fallback={<Text text="Loading..." />}>
1774
+ <AsyncText text={page} ms={5000} />
1775
+ </Suspense>
1776
+ );
1777
+ }
1778
+
1779
+ ReactNoop.render(<App />);
1780
+ expect(Scheduler).toFlushAndYield([]);
1781
+
1782
+ // Initial render.
1783
+ await ReactNoop.act(async () => {
1784
+ React.unstable_withSuspenseConfig(
1785
+ () => transitionToPage('A'),
1786
+ SUSPENSE_CONFIG,
1787
+ );
1788
+
1789
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
1790
+ // Only a short time is needed to unsuspend the initial loading state.
1791
+ Scheduler.advanceTime(400);
1792
+ await advanceTimers(400);
1793
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1794
+ });
1795
+
1796
+ // Later we load the data.
1797
+ Scheduler.advanceTime(5000);
1798
+ await advanceTimers(5000);
1799
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1800
+ expect(Scheduler).toFlushAndYield(['A']);
1801
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1802
+
1803
+ // Start transition.
1804
+ await ReactNoop.act(async () => {
1805
+ React.unstable_withSuspenseConfig(
1806
+ () => transitionToPage('B'),
1807
+ SUSPENSE_CONFIG,
1808
+ );
1809
+
1810
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
1811
+ Scheduler.advanceTime(1000);
1812
+ await advanceTimers(1000);
1813
+ // Even after a second, we have still not yet flushed the loading state.
1814
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1815
+ Scheduler.advanceTime(1100);
1816
+ await advanceTimers(1100);
1817
+ // After the timeout, we do show the loading state.
1818
+ expect(ReactNoop.getChildren()).toEqual([
1819
+ hiddenSpan('A'),
1820
+ span('Loading...'),
1821
+ ]);
1822
+ });
1823
+ // Later we load the data.
1824
+ Scheduler.advanceTime(3000);
1825
+ await advanceTimers(3000);
1826
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1827
+ expect(Scheduler).toFlushAndYield(['B']);
1828
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1829
+ });
1830
+
1831
+ it('classes', async () => {
1832
+ let transitionToPage;
1833
+ class App extends React.Component {
1834
+ state = {page: 'none'};
1835
+ render() {
1836
+ transitionToPage = page => this.setState({page});
1837
+ let page = this.state.page;
1838
+ if (page === 'none') {
1839
+ return null;
1840
+ }
1841
+ return (
1842
+ <Suspense fallback={<Text text="Loading..." />}>
1843
+ <AsyncText text={page} ms={5000} />
1844
+ </Suspense>
1845
+ );
1846
+ }
1847
+ }
1848
+
1849
+ ReactNoop.render(<App />);
1850
+ expect(Scheduler).toFlushAndYield([]);
1851
+
1852
+ // Initial render.
1853
+ await ReactNoop.act(async () => {
1854
+ React.unstable_withSuspenseConfig(
1855
+ () => transitionToPage('A'),
1856
+ SUSPENSE_CONFIG,
1857
+ );
1858
+
1859
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
1860
+ // Only a short time is needed to unsuspend the initial loading state.
1861
+ Scheduler.advanceTime(400);
1862
+ await advanceTimers(400);
1863
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1864
+ });
1865
+
1866
+ // Later we load the data.
1867
+ Scheduler.advanceTime(5000);
1868
+ await advanceTimers(5000);
1869
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1870
+ expect(Scheduler).toFlushAndYield(['A']);
1871
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1872
+
1873
+ // Start transition.
1874
+ await ReactNoop.act(async () => {
1875
+ React.unstable_withSuspenseConfig(
1876
+ () => transitionToPage('B'),
1877
+ SUSPENSE_CONFIG,
1878
+ );
1879
+
1880
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
1881
+ Scheduler.advanceTime(1000);
1882
+ await advanceTimers(1000);
1883
+ // Even after a second, we have still not yet flushed the loading state.
1884
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1885
+ Scheduler.advanceTime(1100);
1886
+ await advanceTimers(1100);
1887
+ // After the timeout, we do show the loading state.
1888
+ expect(ReactNoop.getChildren()).toEqual([
1889
+ hiddenSpan('A'),
1890
+ span('Loading...'),
1891
+ ]);
1892
+ });
1893
+ // Later we load the data.
1894
+ Scheduler.advanceTime(3000);
1895
+ await advanceTimers(3000);
1896
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1897
+ expect(Scheduler).toFlushAndYield(['B']);
1898
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1899
+ });
1900
+ });
1901
+
1902
+ it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
1903
+ function App({page}) {
1904
+ return (
1905
+ <Suspense fallback={<Text text="Loading..." />}>
1906
+ <AsyncText text={page} ms={2000} />
1907
+ </Suspense>
1908
+ );
1909
+ }
1910
+
1911
+ // Initial render.
1912
+ ReactNoop.render(<App page="A" />);
1913
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
1914
+ Scheduler.advanceTime(2000);
1915
+ await advanceTimers(2000);
1916
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1917
+ expect(Scheduler).toFlushAndYield(['A']);
1918
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1919
+
1920
+ // Start transition.
1921
+ React.unstable_withSuspenseConfig(
1922
+ () => {
1923
+ // When we schedule an inner transition without a suspense config
1924
+ // so it should only suspend for a short time.
1925
+ React.unstable_withSuspenseConfig(() =>
1926
+ ReactNoop.render(<App page="B" />),
1927
+ );
1928
+ },
1929
+ {timeoutMs: 2000},
1930
+ );
1931
+
1932
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
1933
+ // Suspended
1934
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1935
+ Scheduler.advanceTime(500);
1936
+ await advanceTimers(500);
1937
+ // Committed loading state.
1938
+ expect(ReactNoop.getChildren()).toEqual([
1939
+ hiddenSpan('A'),
1940
+ span('Loading...'),
1941
+ ]);
1942
+
1943
+ Scheduler.advanceTime(2000);
1944
+ await advanceTimers(2000);
1945
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1946
+ expect(Scheduler).toFlushAndYield(['B']);
1947
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1948
+
1949
+ React.unstable_withSuspenseConfig(
1950
+ () => {
1951
+ // First we schedule an inner unrelated update.
1952
+ React.unstable_withSuspenseConfig(() =>
1953
+ ReactNoop.render(<App page="B" unrelated={true} />),
1954
+ );
1955
+ // Then we schedule another transition to a slow page,
1956
+ // but at this scope we should suspend for longer.
1957
+ Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
1958
+ },
1959
+ {timeoutMs: 2000},
1960
+ );
1961
+ expect(Scheduler).toFlushAndYield([
1962
+ 'Suspend! [C]',
1963
+ 'Loading...',
1964
+ 'Suspend! [C]',
1965
+ 'Loading...',
1966
+ ]);
1967
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1968
+ Scheduler.advanceTime(1200);
1969
+ await advanceTimers(1200);
1970
+ // Even after a second, we have still not yet flushed the loading state.
1971
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
1972
+ Scheduler.advanceTime(1200);
1973
+ await advanceTimers(1200);
1974
+ // After the two second timeout we show the loading state.
1975
+ expect(ReactNoop.getChildren()).toEqual([
1976
+ hiddenSpan('B'),
1977
+ span('Loading...'),
1978
+ ]);
1979
+ });
1980
+
1981
+ it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
1982
+ function App({page}) {
1983
+ return (
1984
+ <Suspense fallback={<Text text="Loading..." />}>
1985
+ <Text text="Hi!" />
1986
+ <Suspense
1987
+ fallback={<Text text={'Loading ' + page + '...'} />}
1988
+ unstable_avoidThisFallback={true}>
1989
+ <AsyncText text={page} ms={3000} />
1990
+ </Suspense>
1991
+ </Suspense>
1992
+ );
1993
+ }
1994
+
1995
+ // Initial render.
1996
+ ReactNoop.render(<App page="A" />);
1997
+ expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [A]', 'Loading...']);
1998
+ Scheduler.advanceTime(3000);
1999
+ await advanceTimers(3000);
2000
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2001
+ expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2002
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2003
+
2004
+ // Start transition.
2005
+ React.unstable_withSuspenseConfig(
2006
+ () => ReactNoop.render(<App page="B" />),
2007
+ {timeoutMs: 2000},
2008
+ );
2009
+
2010
+ expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [B]', 'Loading B...']);
2011
+
2012
+ // Suspended
2013
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2014
+ Scheduler.advanceTime(1800);
2015
+ await advanceTimers(1800);
2016
+ expect(Scheduler).toFlushAndYield([]);
2017
+ // We should still be suspended here because this loading state should be avoided.
2018
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2019
+ Scheduler.advanceTime(1500);
2020
+ await advanceTimers(1500);
2021
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2022
+ expect(ReactNoop.getChildren()).toEqual([
2023
+ span('Hi!'),
2024
+ hiddenSpan('A'),
2025
+ span('Loading B...'),
2026
+ ]);
2027
+ });
2028
+
2029
+ it('withSuspenseConfig timeout applies when we use a newly created avoided boundary', async () => {
2030
+ function App({page}) {
2031
+ return (
2032
+ <Suspense fallback={<Text text="Loading..." />}>
2033
+ <Text text="Hi!" />
2034
+ {page === 'A' ? (
2035
+ <Text text="A" />
2036
+ ) : (
2037
+ <Suspense
2038
+ fallback={<Text text={'Loading ' + page + '...'} />}
2039
+ unstable_avoidThisFallback={true}>
2040
+ <AsyncText text={page} ms={3000} />
2041
+ </Suspense>
2042
+ )}
2043
+ </Suspense>
2044
+ );
2045
+ }
2046
+
2047
+ // Initial render.
2048
+ ReactNoop.render(<App page="A" />);
2049
+ expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2050
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2051
+
2052
+ // Start transition.
2053
+ React.unstable_withSuspenseConfig(
2054
+ () => ReactNoop.render(<App page="B" />),
2055
+ {timeoutMs: 2000},
2056
+ );
2057
+
2058
+ expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [B]', 'Loading B...']);
2059
+
2060
+ // Suspended
2061
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2062
+ Scheduler.advanceTime(1800);
2063
+ await advanceTimers(1800);
2064
+ expect(Scheduler).toFlushAndYield([]);
2065
+ // We should still be suspended here because this loading state should be avoided.
2066
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2067
+ Scheduler.advanceTime(1500);
2068
+ await advanceTimers(1500);
2069
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2070
+ expect(ReactNoop.getChildren()).toEqual([
2071
+ span('Hi!'),
2072
+ span('Loading B...'),
2073
+ ]);
2074
+ });
2075
+
2076
+ it('supports delaying a busy spinner from disappearing', async () => {
2077
+ function useLoadingIndicator(config) {
2078
+ let [isLoading, setLoading] = React.useState(false);
2079
+ let start = React.useCallback(
2080
+ cb => {
2081
+ setLoading(true);
2082
+ Scheduler.unstable_next(() =>
2083
+ React.unstable_withSuspenseConfig(() => {
2084
+ setLoading(false);
2085
+ cb();
2086
+ }, config),
2087
+ );
2088
+ },
2089
+ [setLoading, config],
2090
+ );
2091
+ return [isLoading, start];
2092
+ }
2093
+
2094
+ const SUSPENSE_CONFIG = {
2095
+ timeoutMs: 10000,
2096
+ loadingDelayMs: 500,
2097
+ minLoadingDurationMs: 400,
2098
+ };
2099
+
2100
+ let transitionToPage;
2101
+
2102
+ function App() {
2103
+ let [page, setPage] = React.useState('A');
2104
+ let [isLoading, startLoading] = useLoadingIndicator(SUSPENSE_CONFIG);
2105
+ transitionToPage = nextPage => startLoading(() => setPage(nextPage));
2106
+ return (
2107
+ <Fragment>
2108
+ <Text text={page} />
2109
+ {isLoading ? <Text text="L" /> : null}
2110
+ </Fragment>
2111
+ );
2112
+ }
2113
+
2114
+ // Initial render.
2115
+ ReactNoop.render(<App />);
2116
+ expect(Scheduler).toFlushAndYield(['A']);
2117
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2118
+
2119
+ await ReactNoop.act(async () => {
2120
+ transitionToPage('B');
2121
+ // Rendering B is quick and we didn't have enough
2122
+ // time to show the loading indicator.
2123
+ Scheduler.advanceTime(200);
2124
+ await advanceTimers(200);
2125
+ expect(Scheduler).toFlushAndYield(['A', 'L', 'B']);
2126
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2127
+ });
2128
+
2129
+ await ReactNoop.act(async () => {
2130
+ transitionToPage('C');
2131
+ // Rendering C is a bit slower so we've already showed
2132
+ // the loading indicator.
2133
+ Scheduler.advanceTime(600);
2134
+ await advanceTimers(600);
2135
+ expect(Scheduler).toFlushAndYield(['B', 'L', 'C']);
2136
+ // We're technically done now but we haven't shown the
2137
+ // loading indicator for long enough yet so we'll suspend
2138
+ // while we keep it on the screen a bit longer.
2139
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('L')]);
2140
+ Scheduler.advanceTime(400);
2141
+ await advanceTimers(400);
2142
+ expect(ReactNoop.getChildren()).toEqual([span('C')]);
2143
+ });
2144
+
2145
+ await ReactNoop.act(async () => {
2146
+ transitionToPage('D');
2147
+ // Rendering D is very slow so we've already showed
2148
+ // the loading indicator.
2149
+ Scheduler.advanceTime(1000);
2150
+ await advanceTimers(1000);
2151
+ expect(Scheduler).toFlushAndYield(['C', 'L', 'D']);
2152
+ // However, since we exceeded the minimum time to show
2153
+ // the loading indicator, we commit immediately.
2154
+ expect(ReactNoop.getChildren()).toEqual([span('D')]);
2155
+ });
2156
+ });
2157
});