1. 'use strict';
    
  2. 
    
  3. let React;
    
  4. let ReactNoop;
    
  5. let Scheduler;
    
  6. let act;
    
  7. let use;
    
  8. let useDebugValue;
    
  9. let useState;
    
  10. let useMemo;
    
  11. let useEffect;
    
  12. let Suspense;
    
  13. let startTransition;
    
  14. let cache;
    
  15. let pendingTextRequests;
    
  16. let waitFor;
    
  17. let waitForPaint;
    
  18. let assertLog;
    
  19. let waitForAll;
    
  20. let waitForMicrotasks;
    
  21. 
    
  22. describe('ReactUse', () => {
    
  23.   beforeEach(() => {
    
  24.     jest.resetModules();
    
  25. 
    
  26.     React = require('react');
    
  27.     ReactNoop = require('react-noop-renderer');
    
  28.     Scheduler = require('scheduler');
    
  29.     act = require('internal-test-utils').act;
    
  30.     use = React.use;
    
  31.     useDebugValue = React.useDebugValue;
    
  32.     useState = React.useState;
    
  33.     useMemo = React.useMemo;
    
  34.     useEffect = React.useEffect;
    
  35.     Suspense = React.Suspense;
    
  36.     startTransition = React.startTransition;
    
  37.     cache = React.cache;
    
  38. 
    
  39.     const InternalTestUtils = require('internal-test-utils');
    
  40.     waitForAll = InternalTestUtils.waitForAll;
    
  41.     assertLog = InternalTestUtils.assertLog;
    
  42.     waitForPaint = InternalTestUtils.waitForPaint;
    
  43.     waitFor = InternalTestUtils.waitFor;
    
  44.     waitForMicrotasks = InternalTestUtils.waitForMicrotasks;
    
  45. 
    
  46.     pendingTextRequests = new Map();
    
  47.   });
    
  48. 
    
  49.   function resolveTextRequests(text) {
    
  50.     const requests = pendingTextRequests.get(text);
    
  51.     if (requests !== undefined) {
    
  52.       pendingTextRequests.delete(text);
    
  53.       requests.forEach(resolve => resolve(text));
    
  54.     }
    
  55.   }
    
  56. 
    
  57.   function getAsyncText(text) {
    
  58.     // getAsyncText is completely uncached — it performs a new async operation
    
  59.     // every time it's called. During a transition, React should be able to
    
  60.     // unwrap it anyway.
    
  61.     Scheduler.log(`Async text requested [${text}]`);
    
  62.     return new Promise(resolve => {
    
  63.       const requests = pendingTextRequests.get(text);
    
  64.       if (requests !== undefined) {
    
  65.         requests.push(resolve);
    
  66.         pendingTextRequests.set(text, requests);
    
  67.       } else {
    
  68.         pendingTextRequests.set(text, [resolve]);
    
  69.       }
    
  70.     });
    
  71.   }
    
  72. 
    
  73.   function Text({text}) {
    
  74.     Scheduler.log(text);
    
  75.     return text;
    
  76.   }
    
  77. 
    
  78.   // This behavior was intentionally disabled to derisk the rollout of `use`.
    
  79.   // It changes the behavior of old, pre-`use` Suspense implementations. We may
    
  80.   // add this back; however, the plan is to migrate all existing Suspense code
    
  81.   // to `use`, so the extra code probably isn't worth it.
    
  82.   // @gate TODO
    
  83.   test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
    
  84.     let fulfilled = false;
    
  85.     function Async() {
    
  86.       if (fulfilled) {
    
  87.         return <Text text="Async" />;
    
  88.       }
    
  89.       Scheduler.log('Suspend!');
    
  90.       throw Promise.resolve().then(() => {
    
  91.         Scheduler.log('Resolve in microtask');
    
  92.         fulfilled = true;
    
  93.       });
    
  94.     }
    
  95. 
    
  96.     function App() {
    
  97.       return (
    
  98.         <Suspense fallback={<Text text="Loading..." />}>
    
  99.           <Async />
    
  100.         </Suspense>
    
  101.       );
    
  102.     }
    
  103. 
    
  104.     const root = ReactNoop.createRoot();
    
  105.     await act(() => {
    
  106.       startTransition(() => {
    
  107.         root.render(<App />);
    
  108.       });
    
  109.     });
    
  110. 
    
  111.     assertLog([
    
  112.       // React will yield when the async component suspends.
    
  113.       'Suspend!',
    
  114.       'Resolve in microtask',
    
  115. 
    
  116.       // Finished rendering without unwinding the stack or preparing a fallback.
    
  117.       'Async',
    
  118.     ]);
    
  119.     expect(root).toMatchRenderedOutput('Async');
    
  120.   });
    
  121. 
    
  122.   test('if suspended fiber is pinged in a microtask, it does not block a transition from completing', async () => {
    
  123.     let fulfilled = false;
    
  124.     function Async() {
    
  125.       if (fulfilled) {
    
  126.         return <Text text="Async" />;
    
  127.       }
    
  128.       Scheduler.log('Suspend!');
    
  129.       throw Promise.resolve().then(() => {
    
  130.         Scheduler.log('Resolve in microtask');
    
  131.         fulfilled = true;
    
  132.       });
    
  133.     }
    
  134. 
    
  135.     function App() {
    
  136.       return <Async />;
    
  137.     }
    
  138. 
    
  139.     const root = ReactNoop.createRoot();
    
  140.     await act(() => {
    
  141.       startTransition(() => {
    
  142.         root.render(<App />);
    
  143.       });
    
  144.     });
    
  145.     assertLog(['Suspend!', 'Resolve in microtask', 'Async']);
    
  146.     expect(root).toMatchRenderedOutput('Async');
    
  147.   });
    
  148. 
    
  149.   test('does not infinite loop if already fulfilled thenable is thrown', async () => {
    
  150.     // An already fulfilled promise should never be thrown. Since it already
    
  151.     // fulfilled, we shouldn't bother trying to render again — doing so would
    
  152.     // likely lead to an infinite loop. This scenario should only happen if a
    
  153.     // userspace Suspense library makes an implementation mistake.
    
  154. 
    
  155.     // Create an already fulfilled thenable
    
  156.     const thenable = {
    
  157.       then(ping) {},
    
  158.       status: 'fulfilled',
    
  159.       value: null,
    
  160.     };
    
  161. 
    
  162.     let i = 0;
    
  163.     function Async() {
    
  164.       if (i++ > 50) {
    
  165.         throw new Error('Infinite loop detected');
    
  166.       }
    
  167.       Scheduler.log('Suspend!');
    
  168.       // This thenable should never be thrown because it already fulfilled.
    
  169.       // But if it is thrown, React should handle it gracefully.
    
  170.       throw thenable;
    
  171.     }
    
  172. 
    
  173.     function App() {
    
  174.       return (
    
  175.         <Suspense fallback={<Text text="Loading..." />}>
    
  176.           <Async />
    
  177.         </Suspense>
    
  178.       );
    
  179.     }
    
  180. 
    
  181.     const root = ReactNoop.createRoot();
    
  182.     await act(() => {
    
  183.       root.render(<App />);
    
  184.     });
    
  185.     assertLog(['Suspend!', 'Loading...']);
    
  186.     expect(root).toMatchRenderedOutput('Loading...');
    
  187.   });
    
  188. 
    
  189.   test('basic use(promise)', async () => {
    
  190.     const promiseA = Promise.resolve('A');
    
  191.     const promiseB = Promise.resolve('B');
    
  192.     const promiseC = Promise.resolve('C');
    
  193. 
    
  194.     function Async() {
    
  195.       const text = use(promiseA) + use(promiseB) + use(promiseC);
    
  196.       return <Text text={text} />;
    
  197.     }
    
  198. 
    
  199.     function App() {
    
  200.       return (
    
  201.         <Suspense fallback={<Text text="Loading..." />}>
    
  202.           <Async />
    
  203.         </Suspense>
    
  204.       );
    
  205.     }
    
  206. 
    
  207.     const root = ReactNoop.createRoot();
    
  208.     await act(() => {
    
  209.       startTransition(() => {
    
  210.         root.render(<App />);
    
  211.       });
    
  212.     });
    
  213.     assertLog(['ABC']);
    
  214.     expect(root).toMatchRenderedOutput('ABC');
    
  215.   });
    
  216. 
    
  217.   test("using a promise that's not cached between attempts", async () => {
    
  218.     function Async() {
    
  219.       const text =
    
  220.         use(Promise.resolve('A')) +
    
  221.         use(Promise.resolve('B')) +
    
  222.         use(Promise.resolve('C'));
    
  223.       return <Text text={text} />;
    
  224.     }
    
  225. 
    
  226.     function App() {
    
  227.       return (
    
  228.         <Suspense fallback={<Text text="Loading..." />}>
    
  229.           <Async />
    
  230.         </Suspense>
    
  231.       );
    
  232.     }
    
  233. 
    
  234.     const root = ReactNoop.createRoot();
    
  235.     await act(() => {
    
  236.       startTransition(() => {
    
  237.         root.render(<App />);
    
  238.       });
    
  239.     });
    
  240.     assertLog(['ABC']);
    
  241.     expect(root).toMatchRenderedOutput('ABC');
    
  242.   });
    
  243. 
    
  244.   test('using a rejected promise will throw', async () => {
    
  245.     class ErrorBoundary extends React.Component {
    
  246.       state = {error: null};
    
  247.       static getDerivedStateFromError(error) {
    
  248.         return {error};
    
  249.       }
    
  250.       render() {
    
  251.         if (this.state.error) {
    
  252.           return <Text text={this.state.error.message} />;
    
  253.         }
    
  254.         return this.props.children;
    
  255.       }
    
  256.     }
    
  257. 
    
  258.     const promiseA = Promise.resolve('A');
    
  259.     const promiseB = Promise.reject(new Error('Oops!'));
    
  260.     const promiseC = Promise.resolve('C');
    
  261. 
    
  262.     // Jest/Node will raise an unhandled rejected error unless we await this. It
    
  263.     // works fine in the browser, though.
    
  264.     await expect(promiseB).rejects.toThrow('Oops!');
    
  265. 
    
  266.     function Async() {
    
  267.       const text = use(promiseA) + use(promiseB) + use(promiseC);
    
  268.       return <Text text={text} />;
    
  269.     }
    
  270. 
    
  271.     function App() {
    
  272.       return (
    
  273.         <ErrorBoundary>
    
  274.           <Async />
    
  275.         </ErrorBoundary>
    
  276.       );
    
  277.     }
    
  278. 
    
  279.     const root = ReactNoop.createRoot();
    
  280.     await act(() => {
    
  281.       startTransition(() => {
    
  282.         root.render(<App />);
    
  283.       });
    
  284.     });
    
  285.     assertLog(['Oops!', 'Oops!']);
    
  286.   });
    
  287. 
    
  288.   test('use(promise) in multiple components', async () => {
    
  289.     // This tests that the state for tracking promises is reset per component.
    
  290.     const promiseA = Promise.resolve('A');
    
  291.     const promiseB = Promise.resolve('B');
    
  292.     const promiseC = Promise.resolve('C');
    
  293.     const promiseD = Promise.resolve('D');
    
  294. 
    
  295.     function Child({prefix}) {
    
  296.       return <Text text={prefix + use(promiseC) + use(promiseD)} />;
    
  297.     }
    
  298. 
    
  299.     function Parent() {
    
  300.       return <Child prefix={use(promiseA) + use(promiseB)} />;
    
  301.     }
    
  302. 
    
  303.     function App() {
    
  304.       return (
    
  305.         <Suspense fallback={<Text text="Loading..." />}>
    
  306.           <Parent />
    
  307.         </Suspense>
    
  308.       );
    
  309.     }
    
  310. 
    
  311.     const root = ReactNoop.createRoot();
    
  312.     await act(() => {
    
  313.       startTransition(() => {
    
  314.         root.render(<App />);
    
  315.       });
    
  316.     });
    
  317.     assertLog(['ABCD']);
    
  318.     expect(root).toMatchRenderedOutput('ABCD');
    
  319.   });
    
  320. 
    
  321.   test('use(promise) in multiple sibling components', async () => {
    
  322.     // This tests that the state for tracking promises is reset per component.
    
  323. 
    
  324.     const promiseA = {then: () => {}, status: 'pending', value: null};
    
  325.     const promiseB = {then: () => {}, status: 'pending', value: null};
    
  326.     const promiseC = {then: () => {}, status: 'fulfilled', value: 'C'};
    
  327.     const promiseD = {then: () => {}, status: 'fulfilled', value: 'D'};
    
  328. 
    
  329.     function Sibling1({prefix}) {
    
  330.       return <Text text={use(promiseA) + use(promiseB)} />;
    
  331.     }
    
  332. 
    
  333.     function Sibling2() {
    
  334.       return <Text text={use(promiseC) + use(promiseD)} />;
    
  335.     }
    
  336. 
    
  337.     function App() {
    
  338.       return (
    
  339.         <Suspense fallback={<Text text="Loading..." />}>
    
  340.           <Sibling1 />
    
  341.           <Sibling2 />
    
  342.         </Suspense>
    
  343.       );
    
  344.     }
    
  345. 
    
  346.     const root = ReactNoop.createRoot();
    
  347.     await act(() => {
    
  348.       startTransition(() => {
    
  349.         root.render(<App />);
    
  350.       });
    
  351.     });
    
  352.     assertLog(['Loading...']);
    
  353.     expect(root).toMatchRenderedOutput('Loading...');
    
  354.   });
    
  355. 
    
  356.   test('erroring in the same component as an uncached promise does not result in an infinite loop', async () => {
    
  357.     class ErrorBoundary extends React.Component {
    
  358.       state = {error: null};
    
  359.       static getDerivedStateFromError(error) {
    
  360.         return {error};
    
  361.       }
    
  362.       render() {
    
  363.         if (this.state.error) {
    
  364.           return <Text text={'Caught an error: ' + this.state.error.message} />;
    
  365.         }
    
  366.         return this.props.children;
    
  367.       }
    
  368.     }
    
  369. 
    
  370.     let i = 0;
    
  371.     function Async({
    
  372.       // Intentionally destrucutring a prop here so that our production error
    
  373.       // stack trick is triggered at the beginning of the function
    
  374.       prop,
    
  375.     }) {
    
  376.       if (i++ > 50) {
    
  377.         throw new Error('Infinite loop detected');
    
  378.       }
    
  379.       try {
    
  380.         use(Promise.resolve('Async'));
    
  381.       } catch (e) {
    
  382.         Scheduler.log('Suspend! [Async]');
    
  383.         throw e;
    
  384.       }
    
  385.       throw new Error('Oops!');
    
  386.     }
    
  387. 
    
  388.     function App() {
    
  389.       return (
    
  390.         <Suspense fallback={<Text text="Loading..." />}>
    
  391.           <ErrorBoundary>
    
  392.             <Async />
    
  393.           </ErrorBoundary>
    
  394.         </Suspense>
    
  395.       );
    
  396.     }
    
  397. 
    
  398.     const root = ReactNoop.createRoot();
    
  399.     await act(() => {
    
  400.       startTransition(() => {
    
  401.         root.render(<App />);
    
  402.       });
    
  403.     });
    
  404.     assertLog([
    
  405.       // First attempt. The uncached promise suspends.
    
  406.       'Suspend! [Async]',
    
  407.       // Because the promise already fulfilled, we're able to unwrap the value
    
  408.       // immediately in a microtask.
    
  409.       //
    
  410.       // Then we proceed to the rest of the component, which throws an error.
    
  411.       'Caught an error: Oops!',
    
  412. 
    
  413.       // During the sync error recovery pass, the component suspends, because
    
  414.       // we were unable to unwrap the value of the promise.
    
  415.       'Suspend! [Async]',
    
  416.       'Loading...',
    
  417. 
    
  418.       // Because the error recovery attempt suspended, React can't tell if the
    
  419.       // error was actually fixed, or it was masked by the suspended data.
    
  420.       // In this case, it wasn't actually fixed, so if we were to commit the
    
  421.       // suspended fallback, it would enter an endless error recovery loop.
    
  422.       //
    
  423.       // Instead, we disable error recovery for these lanes and start
    
  424.       // over again.
    
  425. 
    
  426.       // This time, the error is thrown and we commit the result.
    
  427.       'Suspend! [Async]',
    
  428.       'Caught an error: Oops!',
    
  429.     ]);
    
  430.     expect(root).toMatchRenderedOutput('Caught an error: Oops!');
    
  431.   });
    
  432. 
    
  433.   test('basic use(context)', async () => {
    
  434.     const ContextA = React.createContext('');
    
  435.     const ContextB = React.createContext('B');
    
  436. 
    
  437.     function Sync() {
    
  438.       const text = use(ContextA) + use(ContextB);
    
  439.       return text;
    
  440.     }
    
  441. 
    
  442.     function App() {
    
  443.       return (
    
  444.         <ContextA.Provider value="A">
    
  445.           <Sync />
    
  446.         </ContextA.Provider>
    
  447.       );
    
  448.     }
    
  449. 
    
  450.     const root = ReactNoop.createRoot();
    
  451.     root.render(<App />);
    
  452.     await waitForAll([]);
    
  453.     expect(root).toMatchRenderedOutput('AB');
    
  454.   });
    
  455. 
    
  456.   test('interrupting while yielded should reset contexts', async () => {
    
  457.     let resolve;
    
  458.     const promise = new Promise(r => {
    
  459.       resolve = r;
    
  460.     });
    
  461. 
    
  462.     const Context = React.createContext();
    
  463. 
    
  464.     const lazy = React.lazy(() => {
    
  465.       return promise;
    
  466.     });
    
  467. 
    
  468.     function ContextText() {
    
  469.       return <Text text={use(Context)} />;
    
  470.     }
    
  471. 
    
  472.     function App({text}) {
    
  473.       return (
    
  474.         <div>
    
  475.           <Context.Provider value={text}>
    
  476.             {lazy}
    
  477.             <ContextText />
    
  478.           </Context.Provider>
    
  479.         </div>
    
  480.       );
    
  481.     }
    
  482. 
    
  483.     const root = ReactNoop.createRoot();
    
  484.     startTransition(() => {
    
  485.       root.render(<App text="world" />);
    
  486.     });
    
  487.     await waitForPaint([]);
    
  488.     expect(root).toMatchRenderedOutput(null);
    
  489. 
    
  490.     await resolve({default: <Text key="hi" text="Hello " />});
    
  491. 
    
  492.     // Higher priority update that interrupts the first render
    
  493.     ReactNoop.flushSync(() => {
    
  494.       root.render(<App text="world!" />);
    
  495.     });
    
  496. 
    
  497.     assertLog(['Hello ', 'world!']);
    
  498. 
    
  499.     expect(root).toMatchRenderedOutput(<div>Hello world!</div>);
    
  500.   });
    
  501. 
    
  502.   test('warns if use(promise) is wrapped with try/catch block', async () => {
    
  503.     function Async() {
    
  504.       try {
    
  505.         return <Text text={use(Promise.resolve('Async'))} />;
    
  506.       } catch (e) {
    
  507.         return <Text text="Fallback" />;
    
  508.       }
    
  509.     }
    
  510. 
    
  511.     spyOnDev(console, 'error').mockImplementation(() => {});
    
  512.     function App() {
    
  513.       return (
    
  514.         <Suspense fallback={<Text text="Loading..." />}>
    
  515.           <Async />
    
  516.         </Suspense>
    
  517.       );
    
  518.     }
    
  519. 
    
  520.     const root = ReactNoop.createRoot();
    
  521.     await act(() => {
    
  522.       startTransition(() => {
    
  523.         root.render(<App />);
    
  524.       });
    
  525.     });
    
  526. 
    
  527.     if (__DEV__) {
    
  528.       expect(console.error).toHaveBeenCalledTimes(1);
    
  529.       expect(console.error.mock.calls[0][0]).toContain(
    
  530.         'Warning: `use` was called from inside a try/catch block. This is not ' +
    
  531.           'allowed and can lead to unexpected behavior. To handle errors ' +
    
  532.           'triggered by `use`, wrap your component in a error boundary.',
    
  533.       );
    
  534.     }
    
  535.   });
    
  536. 
    
  537.   test('during a transition, can unwrap async operations even if nothing is cached', async () => {
    
  538.     function App() {
    
  539.       return <Text text={use(getAsyncText('Async'))} />;
    
  540.     }
    
  541. 
    
  542.     const root = ReactNoop.createRoot();
    
  543.     await act(() => {
    
  544.       root.render(
    
  545.         <Suspense fallback={<Text text="Loading..." />}>
    
  546.           <Text text="(empty)" />
    
  547.         </Suspense>,
    
  548.       );
    
  549.     });
    
  550.     assertLog(['(empty)']);
    
  551.     expect(root).toMatchRenderedOutput('(empty)');
    
  552. 
    
  553.     await act(() => {
    
  554.       startTransition(() => {
    
  555.         root.render(
    
  556.           <Suspense fallback={<Text text="Loading..." />}>
    
  557.             <App />
    
  558.           </Suspense>,
    
  559.         );
    
  560.       });
    
  561.     });
    
  562.     assertLog(['Async text requested [Async]']);
    
  563.     expect(root).toMatchRenderedOutput('(empty)');
    
  564. 
    
  565.     await act(() => {
    
  566.       resolveTextRequests('Async');
    
  567.     });
    
  568.     assertLog(['Async text requested [Async]', 'Async']);
    
  569.     expect(root).toMatchRenderedOutput('Async');
    
  570.   });
    
  571. 
    
  572.   test("does not prevent a Suspense fallback from showing if it's a new boundary, even during a transition", async () => {
    
  573.     function App() {
    
  574.       return <Text text={use(getAsyncText('Async'))} />;
    
  575.     }
    
  576. 
    
  577.     const root = ReactNoop.createRoot();
    
  578.     await act(() => {
    
  579.       startTransition(() => {
    
  580.         root.render(
    
  581.           <Suspense fallback={<Text text="Loading..." />}>
    
  582.             <App />
    
  583.           </Suspense>,
    
  584.         );
    
  585.       });
    
  586.     });
    
  587.     // Even though the initial render was a transition, it shows a fallback.
    
  588.     assertLog(['Async text requested [Async]', 'Loading...']);
    
  589.     expect(root).toMatchRenderedOutput('Loading...');
    
  590. 
    
  591.     // Resolve the original data
    
  592.     await act(() => {
    
  593.       resolveTextRequests('Async');
    
  594.     });
    
  595.     // During the retry, a fresh request is initiated. Now we must wait for this
    
  596.     // one to finish.
    
  597.     // TODO: This is awkward. Intuitively, you might expect for `act` to wait
    
  598.     // until the new request has finished loading. But if it's mock IO, as in
    
  599.     // this test, how would the developer be able to imperatively flush it if it
    
  600.     // wasn't initiated until the current `act` call? Can't think of a better
    
  601.     // strategy at the moment.
    
  602.     assertLog(['Async text requested [Async]']);
    
  603.     expect(root).toMatchRenderedOutput('Loading...');
    
  604. 
    
  605.     // Flush the second request.
    
  606.     await act(() => {
    
  607.       resolveTextRequests('Async');
    
  608.     });
    
  609.     // This time it finishes because it was during a retry.
    
  610.     assertLog(['Async text requested [Async]', 'Async']);
    
  611.     expect(root).toMatchRenderedOutput('Async');
    
  612.   });
    
  613. 
    
  614.   test('when waiting for data to resolve, a fresh update will trigger a restart', async () => {
    
  615.     function App() {
    
  616.       return <Text text={use(getAsyncText('Will never resolve'))} />;
    
  617.     }
    
  618. 
    
  619.     const root = ReactNoop.createRoot();
    
  620.     await act(() => {
    
  621.       root.render(<Suspense fallback={<Text text="Loading..." />} />);
    
  622.     });
    
  623. 
    
  624.     await act(() => {
    
  625.       startTransition(() => {
    
  626.         root.render(
    
  627.           <Suspense fallback={<Text text="Loading..." />}>
    
  628.             <App />
    
  629.           </Suspense>,
    
  630.         );
    
  631.       });
    
  632.     });
    
  633.     assertLog(['Async text requested [Will never resolve]']);
    
  634. 
    
  635.     await act(() => {
    
  636.       root.render(
    
  637.         <Suspense fallback={<Text text="Loading..." />}>
    
  638.           <Text text="Something different" />
    
  639.         </Suspense>,
    
  640.       );
    
  641.     });
    
  642.     assertLog(['Something different']);
    
  643.   });
    
  644. 
    
  645.   test('when waiting for data to resolve, an update on a different root does not cause work to be dropped', async () => {
    
  646.     const getCachedAsyncText = cache(getAsyncText);
    
  647. 
    
  648.     function App() {
    
  649.       return <Text text={use(getCachedAsyncText('Hi'))} />;
    
  650.     }
    
  651. 
    
  652.     const root1 = ReactNoop.createRoot();
    
  653.     await act(() => {
    
  654.       root1.render(<Suspense fallback={<Text text="Loading..." />} />);
    
  655.     });
    
  656. 
    
  657.     // Start a transition on one root. It will suspend.
    
  658.     await act(() => {
    
  659.       startTransition(() => {
    
  660.         root1.render(
    
  661.           <Suspense fallback={<Text text="Loading..." />}>
    
  662.             <App />
    
  663.           </Suspense>,
    
  664.         );
    
  665.       });
    
  666.     });
    
  667.     assertLog(['Async text requested [Hi]']);
    
  668. 
    
  669.     // While we're waiting for the first root's data to resolve, a second
    
  670.     // root renders.
    
  671.     const root2 = ReactNoop.createRoot();
    
  672.     await act(() => {
    
  673.       root2.render('Do re mi');
    
  674.     });
    
  675.     expect(root2).toMatchRenderedOutput('Do re mi');
    
  676. 
    
  677.     // Once the first root's data is ready, we should finish its transition.
    
  678.     await act(async () => {
    
  679.       await resolveTextRequests('Hi');
    
  680.     });
    
  681.     assertLog(['Hi']);
    
  682.     expect(root1).toMatchRenderedOutput('Hi');
    
  683.   });
    
  684. 
    
  685.   test('while suspended, hooks cannot be called (i.e. current dispatcher is unset correctly)', async () => {
    
  686.     function App() {
    
  687.       return <Text text={use(getAsyncText('Will never resolve'))} />;
    
  688.     }
    
  689. 
    
  690.     const root = ReactNoop.createRoot();
    
  691.     await act(() => {
    
  692.       root.render(<Suspense fallback={<Text text="Loading..." />} />);
    
  693.     });
    
  694. 
    
  695.     await act(() => {
    
  696.       startTransition(() => {
    
  697.         root.render(
    
  698.           <Suspense fallback={<Text text="Loading..." />}>
    
  699.             <App />
    
  700.           </Suspense>,
    
  701.         );
    
  702.       });
    
  703.     });
    
  704.     assertLog(['Async text requested [Will never resolve]']);
    
  705. 
    
  706.     // Calling a hook should error because we're oustide of a component.
    
  707.     expect(useState).toThrow(
    
  708.       'Invalid hook call. Hooks can only be called inside of the body of a ' +
    
  709.         'function component.',
    
  710.     );
    
  711.   });
    
  712. 
    
  713.   test('unwraps thenable that fulfills synchronously without suspending', async () => {
    
  714.     function App() {
    
  715.       const thenable = {
    
  716.         then(resolve) {
    
  717.           // This thenable immediately resolves, synchronously, without waiting
    
  718.           // a microtask.
    
  719.           resolve('Hi');
    
  720.         },
    
  721.       };
    
  722.       try {
    
  723.         return <Text text={use(thenable)} />;
    
  724.       } catch {
    
  725.         throw new Error(
    
  726.           '`use` should not suspend because the thenable resolved synchronously.',
    
  727.         );
    
  728.       }
    
  729.     }
    
  730.     // Because the thenable resolves synchronously, we should be able to finish
    
  731.     // rendering synchronously, with no fallback.
    
  732.     const root = ReactNoop.createRoot();
    
  733.     ReactNoop.flushSync(() => {
    
  734.       root.render(<App />);
    
  735.     });
    
  736.     assertLog(['Hi']);
    
  737.     expect(root).toMatchRenderedOutput('Hi');
    
  738.   });
    
  739. 
    
  740.   test('does not suspend indefinitely if an interleaved update was skipped', async () => {
    
  741.     function Child({childShouldSuspend}) {
    
  742.       return (
    
  743.         <Text
    
  744.           text={
    
  745.             childShouldSuspend
    
  746.               ? use(getAsyncText('Will never resolve'))
    
  747.               : 'Child'
    
  748.           }
    
  749.         />
    
  750.       );
    
  751.     }
    
  752. 
    
  753.     let setChildShouldSuspend;
    
  754.     let setShowChild;
    
  755.     function Parent() {
    
  756.       const [showChild, _setShowChild] = useState(true);
    
  757.       setShowChild = _setShowChild;
    
  758. 
    
  759.       const [childShouldSuspend, _setChildShouldSuspend] = useState(false);
    
  760.       setChildShouldSuspend = _setChildShouldSuspend;
    
  761. 
    
  762.       Scheduler.log(
    
  763.         `childShouldSuspend: ${childShouldSuspend}, showChild: ${showChild}`,
    
  764.       );
    
  765.       return showChild ? (
    
  766.         <Child childShouldSuspend={childShouldSuspend} />
    
  767.       ) : (
    
  768.         <Text text="(empty)" />
    
  769.       );
    
  770.     }
    
  771. 
    
  772.     const root = ReactNoop.createRoot();
    
  773.     await act(() => {
    
  774.       root.render(<Parent />);
    
  775.     });
    
  776.     assertLog(['childShouldSuspend: false, showChild: true', 'Child']);
    
  777.     expect(root).toMatchRenderedOutput('Child');
    
  778. 
    
  779.     await act(async () => {
    
  780.       // Perform an update that causes the app to suspend
    
  781.       startTransition(() => {
    
  782.         setChildShouldSuspend(true);
    
  783.       });
    
  784.       await waitFor(['childShouldSuspend: true, showChild: true']);
    
  785.       // While the update is in progress, schedule another update.
    
  786.       startTransition(() => {
    
  787.         setShowChild(false);
    
  788.       });
    
  789.     });
    
  790.     assertLog([
    
  791.       // Because the interleaved update is not higher priority than what we were
    
  792.       // already working on, it won't interrupt. The first update will continue,
    
  793.       // and will suspend.
    
  794.       'Async text requested [Will never resolve]',
    
  795. 
    
  796.       // Instead of waiting for the promise to resolve, React notices there's
    
  797.       // another pending update that it hasn't tried yet. It will switch to
    
  798.       // rendering that instead.
    
  799.       //
    
  800.       // This time, the update hides the component that previous was suspending,
    
  801.       // so it finishes successfully.
    
  802.       'childShouldSuspend: false, showChild: false',
    
  803.       '(empty)',
    
  804. 
    
  805.       // Finally, React attempts to render the first update again. It also
    
  806.       // finishes successfully, because it was rebased on top of the update that
    
  807.       // hid the suspended component.
    
  808.       // NOTE: These this render happened to not be entangled with the previous
    
  809.       // one. If they had been, this update would have been included in the
    
  810.       // previous render, and there wouldn't be an extra one here. This could
    
  811.       // change if we change our entanglement heurstics. Semantically, it
    
  812.       // shouldn't matter, though in general we try to work on transitions in
    
  813.       // parallel whenever possible. So even though in this particular case, the
    
  814.       // extra render is unnecessary, it's a nice property that it wasn't
    
  815.       // entangled with the other transition.
    
  816.       'childShouldSuspend: true, showChild: false',
    
  817.       '(empty)',
    
  818.     ]);
    
  819.     expect(root).toMatchRenderedOutput('(empty)');
    
  820.   });
    
  821. 
    
  822.   test('when replaying a suspended component, reuses the hooks computed during the previous attempt (Memo)', async () => {
    
  823.     function ExcitingText({text}) {
    
  824.       // This computes the uppercased version of some text. Pretend it's an
    
  825.       // expensive operation that we want to reuse.
    
  826.       const uppercaseText = useMemo(() => {
    
  827.         Scheduler.log('Compute uppercase: ' + text);
    
  828.         return text.toUpperCase();
    
  829.       }, [text]);
    
  830. 
    
  831.       // This adds an exclamation point to the text. Pretend it's an async
    
  832.       // operation that is sent to a service for processing.
    
  833.       const exclamatoryText = use(getAsyncText(uppercaseText + '!'));
    
  834. 
    
  835.       // This surrounds the text with sparkle emojis. The purpose in this test
    
  836.       // is to show that you can suspend in the middle of a sequence of hooks
    
  837.       // without breaking anything.
    
  838.       const sparklingText = useMemo(() => {
    
  839.         Scheduler.log('Add sparkles: ' + exclamatoryText);
    
  840.         return `✨ ${exclamatoryText} ✨`;
    
  841.       }, [exclamatoryText]);
    
  842. 
    
  843.       return <Text text={sparklingText} />;
    
  844.     }
    
  845. 
    
  846.     const root = ReactNoop.createRoot();
    
  847.     await act(() => {
    
  848.       startTransition(() => {
    
  849.         root.render(<ExcitingText text="Hello" />);
    
  850.       });
    
  851.     });
    
  852.     // Suspends while we wait for the async service to respond.
    
  853.     assertLog(['Compute uppercase: Hello', 'Async text requested [HELLO!]']);
    
  854.     expect(root).toMatchRenderedOutput(null);
    
  855. 
    
  856.     // The data is received.
    
  857.     await act(() => {
    
  858.       resolveTextRequests('HELLO!');
    
  859.     });
    
  860.     assertLog([
    
  861.       // We shouldn't run the uppercase computation again, because we can reuse
    
  862.       // the computation from the previous attempt.
    
  863.       // 'Compute uppercase: Hello',
    
  864. 
    
  865.       'Async text requested [HELLO!]',
    
  866.       'Add sparkles: HELLO!',
    
  867.       '✨ HELLO! ✨',
    
  868.     ]);
    
  869.   });
    
  870. 
    
  871.   test('when replaying a suspended component, reuses the hooks computed during the previous attempt (State)', async () => {
    
  872.     let _setFruit;
    
  873.     let _setVegetable;
    
  874.     function Kitchen() {
    
  875.       const [fruit, setFruit] = useState('apple');
    
  876.       _setFruit = setFruit;
    
  877.       const usedFruit = use(getAsyncText(fruit));
    
  878.       const [vegetable, setVegetable] = useState('carrot');
    
  879.       _setVegetable = setVegetable;
    
  880.       return <Text text={usedFruit + ' ' + vegetable} />;
    
  881.     }
    
  882. 
    
  883.     // Initial render.
    
  884.     const root = ReactNoop.createRoot();
    
  885.     await act(() => {
    
  886.       startTransition(() => {
    
  887.         root.render(<Kitchen />);
    
  888.       });
    
  889.     });
    
  890.     assertLog(['Async text requested [apple]']);
    
  891.     expect(root).toMatchRenderedOutput(null);
    
  892.     await act(() => {
    
  893.       resolveTextRequests('apple');
    
  894.     });
    
  895.     assertLog(['Async text requested [apple]', 'apple carrot']);
    
  896.     expect(root).toMatchRenderedOutput('apple carrot');
    
  897. 
    
  898.     // Update the state variable after the use().
    
  899.     await act(() => {
    
  900.       startTransition(() => {
    
  901.         _setVegetable('dill');
    
  902.       });
    
  903.     });
    
  904.     assertLog(['Async text requested [apple]']);
    
  905.     expect(root).toMatchRenderedOutput('apple carrot');
    
  906.     await act(() => {
    
  907.       resolveTextRequests('apple');
    
  908.     });
    
  909.     assertLog(['Async text requested [apple]', 'apple dill']);
    
  910.     expect(root).toMatchRenderedOutput('apple dill');
    
  911. 
    
  912.     // Update the state variable before the use(). The second state is maintained.
    
  913.     await act(() => {
    
  914.       startTransition(() => {
    
  915.         _setFruit('banana');
    
  916.       });
    
  917.     });
    
  918.     assertLog(['Async text requested [banana]']);
    
  919.     expect(root).toMatchRenderedOutput('apple dill');
    
  920.     await act(() => {
    
  921.       resolveTextRequests('banana');
    
  922.     });
    
  923.     assertLog(['Async text requested [banana]', 'banana dill']);
    
  924.     expect(root).toMatchRenderedOutput('banana dill');
    
  925.   });
    
  926. 
    
  927.   test('when replaying a suspended component, reuses the hooks computed during the previous attempt (DebugValue+State)', async () => {
    
  928.     // Make sure we don't get a Hook mismatch warning on updates if there were non-stateful Hooks before the use().
    
  929.     let _setLawyer;
    
  930.     function Lexicon() {
    
  931.       useDebugValue(123);
    
  932.       const avocado = use(getAsyncText('aguacate'));
    
  933.       const [lawyer, setLawyer] = useState('abogado');
    
  934.       _setLawyer = setLawyer;
    
  935.       return <Text text={avocado + ' ' + lawyer} />;
    
  936.     }
    
  937. 
    
  938.     // Initial render.
    
  939.     const root = ReactNoop.createRoot();
    
  940.     await act(() => {
    
  941.       startTransition(() => {
    
  942.         root.render(<Lexicon />);
    
  943.       });
    
  944.     });
    
  945.     assertLog(['Async text requested [aguacate]']);
    
  946.     expect(root).toMatchRenderedOutput(null);
    
  947.     await act(() => {
    
  948.       resolveTextRequests('aguacate');
    
  949.     });
    
  950.     assertLog(['Async text requested [aguacate]', 'aguacate abogado']);
    
  951.     expect(root).toMatchRenderedOutput('aguacate abogado');
    
  952. 
    
  953.     // Now update the state.
    
  954.     await act(() => {
    
  955.       startTransition(() => {
    
  956.         _setLawyer('avocat');
    
  957.       });
    
  958.     });
    
  959.     assertLog(['Async text requested [aguacate]']);
    
  960.     expect(root).toMatchRenderedOutput('aguacate abogado');
    
  961.     await act(() => {
    
  962.       resolveTextRequests('aguacate');
    
  963.     });
    
  964.     assertLog(['Async text requested [aguacate]', 'aguacate avocat']);
    
  965.     expect(root).toMatchRenderedOutput('aguacate avocat');
    
  966.   });
    
  967. 
    
  968.   test(
    
  969.     'wrap an async function with useMemo to skip running the function ' +
    
  970.       'twice when loading new data',
    
  971.     async () => {
    
  972.       function App({text}) {
    
  973.         const promiseForText = useMemo(async () => getAsyncText(text), [text]);
    
  974.         const asyncText = use(promiseForText);
    
  975.         return <Text text={asyncText} />;
    
  976.       }
    
  977. 
    
  978.       const root = ReactNoop.createRoot();
    
  979.       await act(() => {
    
  980.         startTransition(() => {
    
  981.           root.render(<App text="Hello" />);
    
  982.         });
    
  983.       });
    
  984.       assertLog(['Async text requested [Hello]']);
    
  985.       expect(root).toMatchRenderedOutput(null);
    
  986. 
    
  987.       await act(() => {
    
  988.         resolveTextRequests('Hello');
    
  989.       });
    
  990.       assertLog([
    
  991.         // We shouldn't request async text again, because the async function
    
  992.         // was memoized
    
  993.         // 'Async text requested [Hello]'
    
  994. 
    
  995.         'Hello',
    
  996.       ]);
    
  997.     },
    
  998.   );
    
  999. 
    
  1000.   test('load multiple nested Suspense boundaries', async () => {
    
  1001.     const getCachedAsyncText = cache(getAsyncText);
    
  1002. 
    
  1003.     function AsyncText({text}) {
    
  1004.       return <Text text={use(getCachedAsyncText(text))} />;
    
  1005.     }
    
  1006. 
    
  1007.     const root = ReactNoop.createRoot();
    
  1008.     await act(() => {
    
  1009.       root.render(
    
  1010.         <Suspense fallback={<Text text="(Loading A...)" />}>
    
  1011.           <AsyncText text="A" />
    
  1012.           <Suspense fallback={<Text text="(Loading B...)" />}>
    
  1013.             <AsyncText text="B" />
    
  1014.             <Suspense fallback={<Text text="(Loading C...)" />}>
    
  1015.               <AsyncText text="C" />
    
  1016.             </Suspense>
    
  1017.           </Suspense>
    
  1018.         </Suspense>,
    
  1019.       );
    
  1020.     });
    
  1021.     assertLog(['Async text requested [A]', '(Loading A...)']);
    
  1022.     expect(root).toMatchRenderedOutput('(Loading A...)');
    
  1023. 
    
  1024.     await act(() => {
    
  1025.       resolveTextRequests('A');
    
  1026.     });
    
  1027.     assertLog(['A', 'Async text requested [B]', '(Loading B...)']);
    
  1028.     expect(root).toMatchRenderedOutput('A(Loading B...)');
    
  1029. 
    
  1030.     await act(() => {
    
  1031.       resolveTextRequests('B');
    
  1032.     });
    
  1033.     assertLog(['B', 'Async text requested [C]', '(Loading C...)']);
    
  1034.     expect(root).toMatchRenderedOutput('AB(Loading C...)');
    
  1035. 
    
  1036.     await act(() => {
    
  1037.       resolveTextRequests('C');
    
  1038.     });
    
  1039.     assertLog(['C']);
    
  1040.     expect(root).toMatchRenderedOutput('ABC');
    
  1041.   });
    
  1042. 
    
  1043.   test('load multiple nested Suspense boundaries (uncached requests)', async () => {
    
  1044.     // This the same as the previous test, except the requests are not cached.
    
  1045.     // The tree should still eventually resolve, despite the
    
  1046.     // duplicate requests.
    
  1047.     function AsyncText({text}) {
    
  1048.       // This initiates a new request on each render.
    
  1049.       return <Text text={use(getAsyncText(text))} />;
    
  1050.     }
    
  1051. 
    
  1052.     const root = ReactNoop.createRoot();
    
  1053.     await act(() => {
    
  1054.       root.render(
    
  1055.         <Suspense fallback={<Text text="(Loading A...)" />}>
    
  1056.           <AsyncText text="A" />
    
  1057.           <Suspense fallback={<Text text="(Loading B...)" />}>
    
  1058.             <AsyncText text="B" />
    
  1059.             <Suspense fallback={<Text text="(Loading C...)" />}>
    
  1060.               <AsyncText text="C" />
    
  1061.             </Suspense>
    
  1062.           </Suspense>
    
  1063.         </Suspense>,
    
  1064.       );
    
  1065.     });
    
  1066.     assertLog(['Async text requested [A]', '(Loading A...)']);
    
  1067.     expect(root).toMatchRenderedOutput('(Loading A...)');
    
  1068. 
    
  1069.     await act(() => {
    
  1070.       resolveTextRequests('A');
    
  1071.     });
    
  1072.     assertLog(['Async text requested [A]']);
    
  1073.     expect(root).toMatchRenderedOutput('(Loading A...)');
    
  1074. 
    
  1075.     await act(() => {
    
  1076.       resolveTextRequests('A');
    
  1077.     });
    
  1078.     assertLog([
    
  1079.       // React suspends until A finishes loading.
    
  1080.       'Async text requested [A]',
    
  1081.       'A',
    
  1082. 
    
  1083.       // Now React can continue rendering the rest of the tree.
    
  1084. 
    
  1085.       // React does not suspend on the inner requests, because that would
    
  1086.       // block A from appearing. Instead it shows a fallback.
    
  1087.       'Async text requested [B]',
    
  1088.       '(Loading B...)',
    
  1089.     ]);
    
  1090.     expect(root).toMatchRenderedOutput('A(Loading B...)');
    
  1091. 
    
  1092.     await act(() => {
    
  1093.       resolveTextRequests('B');
    
  1094.     });
    
  1095.     assertLog(['Async text requested [B]']);
    
  1096.     expect(root).toMatchRenderedOutput('A(Loading B...)');
    
  1097. 
    
  1098.     await act(() => {
    
  1099.       resolveTextRequests('B');
    
  1100.     });
    
  1101.     assertLog([
    
  1102.       // React suspends until B finishes loading.
    
  1103.       'Async text requested [B]',
    
  1104.       'B',
    
  1105. 
    
  1106.       // React does not suspend on C, because that would block B from appearing.
    
  1107.       'Async text requested [C]',
    
  1108.       '(Loading C...)',
    
  1109.     ]);
    
  1110.     expect(root).toMatchRenderedOutput('AB(Loading C...)');
    
  1111. 
    
  1112.     await act(() => {
    
  1113.       resolveTextRequests('C');
    
  1114.     });
    
  1115.     assertLog(['Async text requested [C]']);
    
  1116.     expect(root).toMatchRenderedOutput('AB(Loading C...)');
    
  1117. 
    
  1118.     await act(() => {
    
  1119.       resolveTextRequests('C');
    
  1120.     });
    
  1121.     assertLog(['Async text requested [C]', 'C']);
    
  1122.     expect(root).toMatchRenderedOutput('ABC');
    
  1123.   });
    
  1124. 
    
  1125.   test('use() combined with render phase updates', async () => {
    
  1126.     function Async() {
    
  1127.       const a = use(Promise.resolve('A'));
    
  1128.       const [count, setCount] = useState(0);
    
  1129.       if (count === 0) {
    
  1130.         setCount(1);
    
  1131.       }
    
  1132.       const usedCount = use(Promise.resolve(count));
    
  1133.       return <Text text={a + usedCount} />;
    
  1134.     }
    
  1135. 
    
  1136.     function App() {
    
  1137.       return (
    
  1138.         <Suspense fallback={<Text text="Loading..." />}>
    
  1139.           <Async />
    
  1140.         </Suspense>
    
  1141.       );
    
  1142.     }
    
  1143. 
    
  1144.     const root = ReactNoop.createRoot();
    
  1145.     await act(() => {
    
  1146.       startTransition(() => {
    
  1147.         root.render(<App />);
    
  1148.       });
    
  1149.     });
    
  1150.     assertLog(['A1']);
    
  1151.     expect(root).toMatchRenderedOutput('A1');
    
  1152.   });
    
  1153. 
    
  1154.   test('basic promise as child', async () => {
    
  1155.     const promise = Promise.resolve(<Text text="Hi" />);
    
  1156.     const root = ReactNoop.createRoot();
    
  1157.     await act(() => {
    
  1158.       startTransition(() => {
    
  1159.         root.render(promise);
    
  1160.       });
    
  1161.     });
    
  1162.     assertLog(['Hi']);
    
  1163.     expect(root).toMatchRenderedOutput('Hi');
    
  1164.   });
    
  1165. 
    
  1166.   test('basic async component', async () => {
    
  1167.     async function App() {
    
  1168.       await getAsyncText('Hi');
    
  1169.       return <Text text="Hi" />;
    
  1170.     }
    
  1171. 
    
  1172.     const root = ReactNoop.createRoot();
    
  1173.     await act(() => {
    
  1174.       startTransition(() => {
    
  1175.         root.render(<App />);
    
  1176.       });
    
  1177.     });
    
  1178.     assertLog(['Async text requested [Hi]']);
    
  1179. 
    
  1180.     await act(() => resolveTextRequests('Hi'));
    
  1181.     assertLog([
    
  1182.       // TODO: We shouldn't have to replay the function body again. Skip
    
  1183.       // straight to reconciliation.
    
  1184.       'Async text requested [Hi]',
    
  1185.       'Hi',
    
  1186.     ]);
    
  1187.     expect(root).toMatchRenderedOutput('Hi');
    
  1188.   });
    
  1189. 
    
  1190.   test('async child of a non-function component (e.g. a class)', async () => {
    
  1191.     class App extends React.Component {
    
  1192.       async render() {
    
  1193.         const text = await getAsyncText('Hi');
    
  1194.         return <Text text={text} />;
    
  1195.       }
    
  1196.     }
    
  1197. 
    
  1198.     const root = ReactNoop.createRoot();
    
  1199.     await act(async () => {
    
  1200.       startTransition(() => {
    
  1201.         root.render(<App />);
    
  1202.       });
    
  1203.     });
    
  1204.     assertLog(['Async text requested [Hi]']);
    
  1205. 
    
  1206.     await act(async () => resolveTextRequests('Hi'));
    
  1207.     assertLog([
    
  1208.       // TODO: We shouldn't have to replay the render function again. We could
    
  1209.       // skip straight to reconciliation. However, it's not as urgent to fix
    
  1210.       // this for fiber types that aren't function components, so we can special
    
  1211.       // case those in the meantime.
    
  1212.       'Async text requested [Hi]',
    
  1213.       'Hi',
    
  1214.     ]);
    
  1215.     expect(root).toMatchRenderedOutput('Hi');
    
  1216.   });
    
  1217. 
    
  1218.   test('async children are recursively unwrapped', async () => {
    
  1219.     // This is a Usable of a Usable. `use` would only unwrap a single level, but
    
  1220.     // when passed as a child, the reconciler recurisvely unwraps until it
    
  1221.     // resolves to a non-Usable value.
    
  1222.     const thenable = {
    
  1223.       then() {},
    
  1224.       status: 'fulfilled',
    
  1225.       value: {
    
  1226.         then() {},
    
  1227.         status: 'fulfilled',
    
  1228.         value: <Text text="Hi" />,
    
  1229.       },
    
  1230.     };
    
  1231.     const root = ReactNoop.createRoot();
    
  1232.     await act(() => {
    
  1233.       root.render(thenable);
    
  1234.     });
    
  1235.     assertLog(['Hi']);
    
  1236.     expect(root).toMatchRenderedOutput('Hi');
    
  1237.   });
    
  1238. 
    
  1239.   test('async children are transparently unwrapped before being reconciled (top level)', async () => {
    
  1240.     function Child({text}) {
    
  1241.       useEffect(() => {
    
  1242.         Scheduler.log(`Mount: ${text}`);
    
  1243.       }, [text]);
    
  1244.       return <Text text={text} />;
    
  1245.     }
    
  1246. 
    
  1247.     async function App({text}) {
    
  1248.       // The child returned by this component is always a promise (async
    
  1249.       // functions always return promises). React should unwrap it and reconcile
    
  1250.       // the result, not the promise itself.
    
  1251.       return <Child text={text} />;
    
  1252.     }
    
  1253. 
    
  1254.     const root = ReactNoop.createRoot();
    
  1255.     await act(() => {
    
  1256.       startTransition(() => {
    
  1257.         root.render(<App text="A" />);
    
  1258.       });
    
  1259.     });
    
  1260.     assertLog(['A', 'Mount: A']);
    
  1261.     expect(root).toMatchRenderedOutput('A');
    
  1262. 
    
  1263.     // Update the child's props. It should not remount.
    
  1264.     await act(() => {
    
  1265.       startTransition(() => {
    
  1266.         root.render(<App text="B" />);
    
  1267.       });
    
  1268.     });
    
  1269.     assertLog(['B', 'Mount: B']);
    
  1270.     expect(root).toMatchRenderedOutput('B');
    
  1271.   });
    
  1272. 
    
  1273.   test('async children are transparently unwrapped before being reconciled (siblings)', async () => {
    
  1274.     function Child({text}) {
    
  1275.       useEffect(() => {
    
  1276.         Scheduler.log(`Mount: ${text}`);
    
  1277.       }, [text]);
    
  1278.       return <Text text={text} />;
    
  1279.     }
    
  1280. 
    
  1281.     const root = ReactNoop.createRoot();
    
  1282.     await act(async () => {
    
  1283.       startTransition(() => {
    
  1284.         root.render(
    
  1285.           <>
    
  1286.             {Promise.resolve(<Child text="A" />)}
    
  1287.             {Promise.resolve(<Child text="B" />)}
    
  1288.             {Promise.resolve(<Child text="C" />)}
    
  1289.           </>,
    
  1290.         );
    
  1291.       });
    
  1292.     });
    
  1293.     assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
    
  1294.     expect(root).toMatchRenderedOutput('ABC');
    
  1295. 
    
  1296.     await act(() => {
    
  1297.       startTransition(() => {
    
  1298.         root.render(
    
  1299.           <>
    
  1300.             {Promise.resolve(<Child text="A" />)}
    
  1301.             {Promise.resolve(<Child text="B" />)}
    
  1302.             {Promise.resolve(<Child text="C" />)}
    
  1303.           </>,
    
  1304.         );
    
  1305.       });
    
  1306.     });
    
  1307.     // Nothing should have remounted
    
  1308.     assertLog(['A', 'B', 'C']);
    
  1309.     expect(root).toMatchRenderedOutput('ABC');
    
  1310.   });
    
  1311. 
    
  1312.   test('async children are transparently unwrapped before being reconciled (siblings, reordered)', async () => {
    
  1313.     function Child({text}) {
    
  1314.       useEffect(() => {
    
  1315.         Scheduler.log(`Mount: ${text}`);
    
  1316.       }, [text]);
    
  1317.       return <Text text={text} />;
    
  1318.     }
    
  1319. 
    
  1320.     const root = ReactNoop.createRoot();
    
  1321.     await act(() => {
    
  1322.       startTransition(() => {
    
  1323.         root.render(
    
  1324.           <>
    
  1325.             {Promise.resolve(<Child key="A" text="A" />)}
    
  1326.             {Promise.resolve(<Child key="B" text="B" />)}
    
  1327.             {Promise.resolve(<Child key="C" text="C" />)}
    
  1328.           </>,
    
  1329.         );
    
  1330.       });
    
  1331.     });
    
  1332.     assertLog(['A', 'B', 'C', 'Mount: A', 'Mount: B', 'Mount: C']);
    
  1333.     expect(root).toMatchRenderedOutput('ABC');
    
  1334. 
    
  1335.     await act(() => {
    
  1336.       startTransition(() => {
    
  1337.         root.render(
    
  1338.           <>
    
  1339.             {Promise.resolve(<Child key="B" text="B" />)}
    
  1340.             {Promise.resolve(<Child key="A" text="A" />)}
    
  1341.             {Promise.resolve(<Child key="C" text="C" />)}
    
  1342.           </>,
    
  1343.         );
    
  1344.       });
    
  1345.     });
    
  1346.     // Nothing should have remounted
    
  1347.     assertLog(['B', 'A', 'C']);
    
  1348.     expect(root).toMatchRenderedOutput('BAC');
    
  1349.   });
    
  1350. 
    
  1351.   test('basic Context as node', async () => {
    
  1352.     const Context = React.createContext(null);
    
  1353. 
    
  1354.     function Indirection({children}) {
    
  1355.       Scheduler.log('Indirection');
    
  1356.       return children;
    
  1357.     }
    
  1358. 
    
  1359.     function ParentOfContextNode() {
    
  1360.       Scheduler.log('ParentOfContextNode');
    
  1361.       return Context;
    
  1362.     }
    
  1363. 
    
  1364.     function Child({text}) {
    
  1365.       useEffect(() => {
    
  1366.         Scheduler.log('Mount');
    
  1367.         return () => {
    
  1368.           Scheduler.log('Unmount');
    
  1369.         };
    
  1370.       }, []);
    
  1371.       return <Text text={text} />;
    
  1372.     }
    
  1373. 
    
  1374.     function App({contextValue, children}) {
    
  1375.       const memoizedChildren = useMemo(
    
  1376.         () => (
    
  1377.           <Indirection>
    
  1378.             <ParentOfContextNode />
    
  1379.           </Indirection>
    
  1380.         ),
    
  1381.         [children],
    
  1382.       );
    
  1383.       return (
    
  1384.         <Context.Provider value={contextValue}>
    
  1385.           {memoizedChildren}
    
  1386.         </Context.Provider>
    
  1387.       );
    
  1388.     }
    
  1389. 
    
  1390.     // Initial render
    
  1391.     const root = ReactNoop.createRoot();
    
  1392.     await act(() => {
    
  1393.       root.render(<App contextValue={<Child text="A" />} />);
    
  1394.     });
    
  1395.     assertLog(['Indirection', 'ParentOfContextNode', 'A', 'Mount']);
    
  1396.     expect(root).toMatchRenderedOutput('A');
    
  1397. 
    
  1398.     // Update the child to a new value
    
  1399.     await act(async () => {
    
  1400.       root.render(<App contextValue={<Child text="B" />} />);
    
  1401.     });
    
  1402.     assertLog([
    
  1403.       // Notice that the <Indirection /> did not rerender, because the
    
  1404.       // update was sent via Context.
    
  1405. 
    
  1406.       // TODO: We shouldn't have to re-render the parent of the context node.
    
  1407.       // This happens because we need to reconcile the parent's children again.
    
  1408.       // However, we should be able to skip directly to reconcilation without
    
  1409.       // evaluating the component. One way to do this might be to mark the
    
  1410.       // context dependency with a flag that says it was added
    
  1411.       // during reconcilation.
    
  1412.       'ParentOfContextNode',
    
  1413. 
    
  1414.       // Notice that this was an update, not a remount.
    
  1415.       'B',
    
  1416.     ]);
    
  1417.     expect(root).toMatchRenderedOutput('B');
    
  1418. 
    
  1419.     // Delete the old child and replace it with a new one, by changing the key
    
  1420.     await act(async () => {
    
  1421.       root.render(<App contextValue={<Child key="C" text="C" />} />);
    
  1422.     });
    
  1423.     assertLog([
    
  1424.       'ParentOfContextNode',
    
  1425. 
    
  1426.       // A new instance is mounted
    
  1427.       'C',
    
  1428.       'Unmount',
    
  1429.       'Mount',
    
  1430.     ]);
    
  1431.   });
    
  1432. 
    
  1433.   test('context as node, at the root', async () => {
    
  1434.     const Context = React.createContext(<Text text="Hi" />);
    
  1435.     const root = ReactNoop.createRoot();
    
  1436.     await act(async () => {
    
  1437.       startTransition(() => {
    
  1438.         root.render(Context);
    
  1439.       });
    
  1440.     });
    
  1441.     assertLog(['Hi']);
    
  1442.     expect(root).toMatchRenderedOutput('Hi');
    
  1443.   });
    
  1444. 
    
  1445.   test('promises that resolves to a context, rendered as a node', async () => {
    
  1446.     const Context = React.createContext(<Text text="Hi" />);
    
  1447.     const promise = Promise.resolve(Context);
    
  1448.     const root = ReactNoop.createRoot();
    
  1449.     await act(async () => {
    
  1450.       startTransition(() => {
    
  1451.         root.render(promise);
    
  1452.       });
    
  1453.     });
    
  1454.     assertLog(['Hi']);
    
  1455.     expect(root).toMatchRenderedOutput('Hi');
    
  1456.   });
    
  1457. 
    
  1458.   test('unwrap uncached promises inside forwardRef', async () => {
    
  1459.     const asyncInstance = {};
    
  1460.     const Async = React.forwardRef((props, ref) => {
    
  1461.       React.useImperativeHandle(ref, () => asyncInstance);
    
  1462.       const text = use(Promise.resolve('Async'));
    
  1463.       return <Text text={text} />;
    
  1464.     });
    
  1465. 
    
  1466.     const ref = React.createRef();
    
  1467.     function App() {
    
  1468.       return (
    
  1469.         <Suspense fallback={<Text text="Loading..." />}>
    
  1470.           <Async ref={ref} />
    
  1471.         </Suspense>
    
  1472.       );
    
  1473.     }
    
  1474. 
    
  1475.     const root = ReactNoop.createRoot();
    
  1476.     await act(() => {
    
  1477.       startTransition(() => {
    
  1478.         root.render(<App />);
    
  1479.       });
    
  1480.     });
    
  1481.     assertLog(['Async']);
    
  1482.     expect(root).toMatchRenderedOutput('Async');
    
  1483.     expect(ref.current).toBe(asyncInstance);
    
  1484.   });
    
  1485. 
    
  1486.   test('unwrap uncached promises inside memo', async () => {
    
  1487.     const Async = React.memo(
    
  1488.       props => {
    
  1489.         const text = use(Promise.resolve(props.text));
    
  1490.         return <Text text={text} />;
    
  1491.       },
    
  1492.       (a, b) => a.text === b.text,
    
  1493.     );
    
  1494. 
    
  1495.     function App({text}) {
    
  1496.       return (
    
  1497.         <Suspense fallback={<Text text="Loading..." />}>
    
  1498.           <Async text={text} />
    
  1499.         </Suspense>
    
  1500.       );
    
  1501.     }
    
  1502. 
    
  1503.     const root = ReactNoop.createRoot();
    
  1504.     await act(() => {
    
  1505.       startTransition(() => {
    
  1506.         root.render(<App text="Async" />);
    
  1507.       });
    
  1508.     });
    
  1509.     assertLog(['Async']);
    
  1510.     expect(root).toMatchRenderedOutput('Async');
    
  1511. 
    
  1512.     // Update to the same value
    
  1513.     await act(() => {
    
  1514.       startTransition(() => {
    
  1515.         root.render(<App text="Async" />);
    
  1516.       });
    
  1517.     });
    
  1518.     // Should not have re-rendered, because it's memoized
    
  1519.     assertLog([]);
    
  1520.     expect(root).toMatchRenderedOutput('Async');
    
  1521. 
    
  1522.     // Update to a different value
    
  1523.     await act(() => {
    
  1524.       startTransition(() => {
    
  1525.         root.render(<App text="Async!" />);
    
  1526.       });
    
  1527.     });
    
  1528.     assertLog(['Async!']);
    
  1529.     expect(root).toMatchRenderedOutput('Async!');
    
  1530.   });
    
  1531. 
    
  1532.   // @gate !disableLegacyContext
    
  1533.   test('unwrap uncached promises in component that accesses legacy context', async () => {
    
  1534.     class ContextProvider extends React.Component {
    
  1535.       static childContextTypes = {
    
  1536.         legacyContext() {},
    
  1537.       };
    
  1538.       getChildContext() {
    
  1539.         return {legacyContext: 'Async'};
    
  1540.       }
    
  1541.       render() {
    
  1542.         return this.props.children;
    
  1543.       }
    
  1544.     }
    
  1545. 
    
  1546.     function Async({label}, context) {
    
  1547.       const text = use(Promise.resolve(context.legacyContext + ` (${label})`));
    
  1548.       return <Text text={text} />;
    
  1549.     }
    
  1550.     Async.contextTypes = {
    
  1551.       legacyContext: () => {},
    
  1552.     };
    
  1553. 
    
  1554.     const AsyncMemo = React.memo(Async, (a, b) => a.label === b.label);
    
  1555. 
    
  1556.     function App() {
    
  1557.       return (
    
  1558.         <ContextProvider>
    
  1559.           <Suspense fallback={<Text text="Loading..." />}>
    
  1560.             <div>
    
  1561.               <Async label="function component" />
    
  1562.             </div>
    
  1563.             <div>
    
  1564.               <AsyncMemo label="memo component" />
    
  1565.             </div>
    
  1566.           </Suspense>
    
  1567.         </ContextProvider>
    
  1568.       );
    
  1569.     }
    
  1570. 
    
  1571.     const root = ReactNoop.createRoot();
    
  1572.     await act(() => {
    
  1573.       startTransition(() => {
    
  1574.         root.render(<App />);
    
  1575.       });
    
  1576.     });
    
  1577.     assertLog(['Async (function component)', 'Async (memo component)']);
    
  1578.     expect(root).toMatchRenderedOutput(
    
  1579.       <>
    
  1580.         <div>Async (function component)</div>
    
  1581.         <div>Async (memo component)</div>
    
  1582.       </>,
    
  1583.     );
    
  1584.   });
    
  1585. 
    
  1586.   test('regression test: updates while component is suspended should not be mistaken for render phase updates', async () => {
    
  1587.     const getCachedAsyncText = cache(getAsyncText);
    
  1588. 
    
  1589.     let setState;
    
  1590.     function App() {
    
  1591.       const [state, _setState] = useState('A');
    
  1592.       setState = _setState;
    
  1593.       return <Text text={use(getCachedAsyncText(state))} />;
    
  1594.     }
    
  1595. 
    
  1596.     // Initial render
    
  1597.     const root = ReactNoop.createRoot();
    
  1598.     await act(() => root.render(<App />));
    
  1599.     assertLog(['Async text requested [A]']);
    
  1600.     expect(root).toMatchRenderedOutput(null);
    
  1601.     await act(() => resolveTextRequests('A'));
    
  1602.     assertLog(['A']);
    
  1603.     expect(root).toMatchRenderedOutput('A');
    
  1604. 
    
  1605.     // Update to B. This will suspend.
    
  1606.     await act(() => startTransition(() => setState('B')));
    
  1607.     assertLog(['Async text requested [B]']);
    
  1608.     expect(root).toMatchRenderedOutput('A');
    
  1609. 
    
  1610.     // While B is suspended, update to C. This should immediately interrupt
    
  1611.     // the render for B. In the regression, this update was mistakenly treated
    
  1612.     // as a render phase update.
    
  1613.     ReactNoop.flushSync(() => setState('C'));
    
  1614.     assertLog(['Async text requested [C]']);
    
  1615. 
    
  1616.     // Finish rendering.
    
  1617.     await act(() => resolveTextRequests('C'));
    
  1618.     assertLog(['C']);
    
  1619.     expect(root).toMatchRenderedOutput('C');
    
  1620.   });
    
  1621. 
    
  1622.   // @gate !forceConcurrentByDefaultForTesting
    
  1623.   test('an async component outside of a Suspense boundary crashes with an error (resolves in microtask)', async () => {
    
  1624.     class ErrorBoundary extends React.Component {
    
  1625.       state = {error: null};
    
  1626.       static getDerivedStateFromError(error) {
    
  1627.         return {error};
    
  1628.       }
    
  1629.       render() {
    
  1630.         if (this.state.error) {
    
  1631.           return <Text text={this.state.error.message} />;
    
  1632.         }
    
  1633.         return this.props.children;
    
  1634.       }
    
  1635.     }
    
  1636. 
    
  1637.     async function AsyncClientComponent() {
    
  1638.       return <Text text="Hi" />;
    
  1639.     }
    
  1640. 
    
  1641.     const root = ReactNoop.createRoot();
    
  1642.     await expect(async () => {
    
  1643.       await act(() => {
    
  1644.         root.render(
    
  1645.           <ErrorBoundary>
    
  1646.             <AsyncClientComponent />
    
  1647.           </ErrorBoundary>,
    
  1648.         );
    
  1649.       });
    
  1650.     }).toErrorDev([
    
  1651.       'async/await is not yet supported in Client Components, only ' +
    
  1652.         'Server Components. This error is often caused by accidentally ' +
    
  1653.         "adding `'use client'` to a module that was originally written " +
    
  1654.         'for the server.',
    
  1655.     ]);
    
  1656.     assertLog([
    
  1657.       'async/await is not yet supported in Client Components, only Server ' +
    
  1658.         'Components. This error is often caused by accidentally adding ' +
    
  1659.         "`'use client'` to a module that was originally written for " +
    
  1660.         'the server.',
    
  1661.       'async/await is not yet supported in Client Components, only Server ' +
    
  1662.         'Components. This error is often caused by accidentally adding ' +
    
  1663.         "`'use client'` to a module that was originally written for " +
    
  1664.         'the server.',
    
  1665.     ]);
    
  1666.     expect(root).toMatchRenderedOutput(
    
  1667.       'async/await is not yet supported in Client Components, only Server ' +
    
  1668.         'Components. This error is often caused by accidentally adding ' +
    
  1669.         "`'use client'` to a module that was originally written for " +
    
  1670.         'the server.',
    
  1671.     );
    
  1672.   });
    
  1673. 
    
  1674.   // @gate !forceConcurrentByDefaultForTesting
    
  1675.   test('an async component outside of a Suspense boundary crashes with an error (resolves in macrotask)', async () => {
    
  1676.     class ErrorBoundary extends React.Component {
    
  1677.       state = {error: null};
    
  1678.       static getDerivedStateFromError(error) {
    
  1679.         return {error};
    
  1680.       }
    
  1681.       render() {
    
  1682.         if (this.state.error) {
    
  1683.           return <Text text={this.state.error.message} />;
    
  1684.         }
    
  1685.         return this.props.children;
    
  1686.       }
    
  1687.     }
    
  1688. 
    
  1689.     async function AsyncClientComponent() {
    
  1690.       await waitForMicrotasks();
    
  1691.       return <Text text="Hi" />;
    
  1692.     }
    
  1693. 
    
  1694.     const root = ReactNoop.createRoot();
    
  1695.     await expect(async () => {
    
  1696.       await act(() => {
    
  1697.         root.render(
    
  1698.           <ErrorBoundary>
    
  1699.             <AsyncClientComponent />
    
  1700.           </ErrorBoundary>,
    
  1701.         );
    
  1702.       });
    
  1703.     }).toErrorDev([
    
  1704.       'async/await is not yet supported in Client Components, only ' +
    
  1705.         'Server Components. This error is often caused by accidentally ' +
    
  1706.         "adding `'use client'` to a module that was originally written " +
    
  1707.         'for the server.',
    
  1708.     ]);
    
  1709.     assertLog([
    
  1710.       'async/await is not yet supported in Client Components, only Server ' +
    
  1711.         'Components. This error is often caused by accidentally adding ' +
    
  1712.         "`'use client'` to a module that was originally written for " +
    
  1713.         'the server.',
    
  1714.       'async/await is not yet supported in Client Components, only Server ' +
    
  1715.         'Components. This error is often caused by accidentally adding ' +
    
  1716.         "`'use client'` to a module that was originally written for " +
    
  1717.         'the server.',
    
  1718.     ]);
    
  1719.     expect(root).toMatchRenderedOutput(
    
  1720.       'async/await is not yet supported in Client Components, only Server ' +
    
  1721.         'Components. This error is often caused by accidentally adding ' +
    
  1722.         "`'use client'` to a module that was originally written for " +
    
  1723.         'the server.',
    
  1724.     );
    
  1725.   });
    
  1726. 
    
  1727.   test('warn if async client component calls a hook (e.g. useState)', async () => {
    
  1728.     async function AsyncClientComponent() {
    
  1729.       useState();
    
  1730.       return <Text text="Hi" />;
    
  1731.     }
    
  1732. 
    
  1733.     const root = ReactNoop.createRoot();
    
  1734.     await expect(async () => {
    
  1735.       await act(() => {
    
  1736.         startTransition(() => {
    
  1737.           root.render(<AsyncClientComponent />);
    
  1738.         });
    
  1739.       });
    
  1740.     }).toErrorDev([
    
  1741.       'Hooks are not supported inside an async component. This ' +
    
  1742.         "error is often caused by accidentally adding `'use client'` " +
    
  1743.         'to a module that was originally written for the server.',
    
  1744.     ]);
    
  1745.   });
    
  1746. 
    
  1747.   test('warn if async client component calls a hook (e.g. use)', async () => {
    
  1748.     const promise = Promise.resolve();
    
  1749. 
    
  1750.     async function AsyncClientComponent() {
    
  1751.       use(promise);
    
  1752.       return <Text text="Hi" />;
    
  1753.     }
    
  1754. 
    
  1755.     const root = ReactNoop.createRoot();
    
  1756.     await expect(async () => {
    
  1757.       await act(() => {
    
  1758.         startTransition(() => {
    
  1759.           root.render(<AsyncClientComponent />);
    
  1760.         });
    
  1761.       });
    
  1762.     }).toErrorDev([
    
  1763.       'Hooks are not supported inside an async component. This ' +
    
  1764.         "error is often caused by accidentally adding `'use client'` " +
    
  1765.         'to a module that was originally written for the server.',
    
  1766.     ]);
    
  1767.   });
    
  1768. });