1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @jest-environment node
    
  8.  */
    
  9. 
    
  10. // sanity tests for act()
    
  11. 
    
  12. let React;
    
  13. let ReactNoop;
    
  14. let act;
    
  15. let use;
    
  16. let Suspense;
    
  17. let DiscreteEventPriority;
    
  18. let startTransition;
    
  19. let waitForMicrotasks;
    
  20. let Scheduler;
    
  21. let assertLog;
    
  22. 
    
  23. describe('isomorphic act()', () => {
    
  24.   beforeEach(() => {
    
  25.     React = require('react');
    
  26.     Scheduler = require('scheduler');
    
  27. 
    
  28.     ReactNoop = require('react-noop-renderer');
    
  29.     DiscreteEventPriority =
    
  30.       require('react-reconciler/constants').DiscreteEventPriority;
    
  31.     act = React.unstable_act;
    
  32.     use = React.use;
    
  33.     Suspense = React.Suspense;
    
  34.     startTransition = React.startTransition;
    
  35. 
    
  36.     waitForMicrotasks = require('internal-test-utils').waitForMicrotasks;
    
  37.     assertLog = require('internal-test-utils').assertLog;
    
  38.   });
    
  39. 
    
  40.   beforeEach(() => {
    
  41.     global.IS_REACT_ACT_ENVIRONMENT = true;
    
  42.   });
    
  43. 
    
  44.   afterEach(() => {
    
  45.     jest.restoreAllMocks();
    
  46.   });
    
  47. 
    
  48.   function Text({text}) {
    
  49.     Scheduler.log(text);
    
  50.     return text;
    
  51.   }
    
  52. 
    
  53.   // @gate __DEV__
    
  54.   test('bypasses queueMicrotask', async () => {
    
  55.     const root = ReactNoop.createRoot();
    
  56. 
    
  57.     // First test what happens without wrapping in act. This update would
    
  58.     // normally be queued in a microtask.
    
  59.     global.IS_REACT_ACT_ENVIRONMENT = false;
    
  60.     ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
    
  61.       root.render('A');
    
  62.     });
    
  63.     // Nothing has rendered yet
    
  64.     expect(root).toMatchRenderedOutput(null);
    
  65.     // Flush the microtasks by awaiting
    
  66.     await waitForMicrotasks();
    
  67.     expect(root).toMatchRenderedOutput('A');
    
  68. 
    
  69.     // Now do the same thing but wrap the update with `act`. No
    
  70.     // `await` necessary.
    
  71.     global.IS_REACT_ACT_ENVIRONMENT = true;
    
  72.     act(() => {
    
  73.       ReactNoop.unstable_runWithPriority(DiscreteEventPriority, () => {
    
  74.         root.render('B');
    
  75.       });
    
  76.     });
    
  77.     expect(root).toMatchRenderedOutput('B');
    
  78.   });
    
  79. 
    
  80.   // @gate __DEV__
    
  81.   test('return value – sync callback', async () => {
    
  82.     expect(await act(() => 'hi')).toEqual('hi');
    
  83.   });
    
  84. 
    
  85.   // @gate __DEV__
    
  86.   test('return value – sync callback, nested', async () => {
    
  87.     const returnValue = await act(() => {
    
  88.       return act(() => 'hi');
    
  89.     });
    
  90.     expect(returnValue).toEqual('hi');
    
  91.   });
    
  92. 
    
  93.   // @gate __DEV__
    
  94.   test('return value – async callback', async () => {
    
  95.     const returnValue = await act(async () => {
    
  96.       return await Promise.resolve('hi');
    
  97.     });
    
  98.     expect(returnValue).toEqual('hi');
    
  99.   });
    
  100. 
    
  101.   // @gate __DEV__
    
  102.   test('return value – async callback, nested', async () => {
    
  103.     const returnValue = await act(async () => {
    
  104.       return await act(async () => {
    
  105.         return await Promise.resolve('hi');
    
  106.       });
    
  107.     });
    
  108.     expect(returnValue).toEqual('hi');
    
  109.   });
    
  110. 
    
  111.   // @gate __DEV__
    
  112.   test('in legacy mode, updates are batched', () => {
    
  113.     const root = ReactNoop.createLegacyRoot();
    
  114. 
    
  115.     // Outside of `act`, legacy updates are flushed completely synchronously
    
  116.     root.render('A');
    
  117.     expect(root).toMatchRenderedOutput('A');
    
  118. 
    
  119.     // `act` will batch the updates and flush them at the end
    
  120.     act(() => {
    
  121.       root.render('B');
    
  122.       // Hasn't flushed yet
    
  123.       expect(root).toMatchRenderedOutput('A');
    
  124. 
    
  125.       // Confirm that a nested `batchedUpdates` call won't cause the updates
    
  126.       // to flush early.
    
  127.       ReactNoop.batchedUpdates(() => {
    
  128.         root.render('C');
    
  129.       });
    
  130. 
    
  131.       // Still hasn't flushed
    
  132.       expect(root).toMatchRenderedOutput('A');
    
  133.     });
    
  134. 
    
  135.     // Now everything renders in a single batch.
    
  136.     expect(root).toMatchRenderedOutput('C');
    
  137.   });
    
  138. 
    
  139.   // @gate __DEV__
    
  140.   test('in legacy mode, in an async scope, updates are batched until the first `await`', async () => {
    
  141.     const root = ReactNoop.createLegacyRoot();
    
  142. 
    
  143.     await act(async () => {
    
  144.       queueMicrotask(() => {
    
  145.         Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
    
  146.         root.render(<Text text="C" />);
    
  147.       });
    
  148.       root.render(<Text text="A" />);
    
  149.       root.render(<Text text="B" />);
    
  150. 
    
  151.       await null;
    
  152.       assertLog([
    
  153.         // A and B should render in a single batch _before_ the microtask queue
    
  154.         // has run. This replicates the behavior of the original `act`
    
  155.         // implementation, for compatibility.
    
  156.         'B',
    
  157.         'Current tree in microtask: B',
    
  158. 
    
  159.         // C isn't scheduled until a microtask, so it's rendered separately.
    
  160.         'C',
    
  161.       ]);
    
  162. 
    
  163.       // Subsequent updates should also render in separate batches.
    
  164.       root.render(<Text text="D" />);
    
  165.       root.render(<Text text="E" />);
    
  166.       assertLog(['D', 'E']);
    
  167.     });
    
  168.   });
    
  169. 
    
  170.   // @gate __DEV__
    
  171.   test('in legacy mode, in an async scope, updates are batched until the first `await` (regression test: batchedUpdates)', async () => {
    
  172.     const root = ReactNoop.createLegacyRoot();
    
  173. 
    
  174.     await act(async () => {
    
  175.       queueMicrotask(() => {
    
  176.         Scheduler.log('Current tree in microtask: ' + root.getChildrenAsJSX());
    
  177.         root.render(<Text text="C" />);
    
  178.       });
    
  179. 
    
  180.       // This is a regression test. The presence of `batchedUpdates` would cause
    
  181.       // these updates to not flush until a microtask. The correct behavior is
    
  182.       // that they flush before the microtask queue, regardless of whether
    
  183.       // they are wrapped with `batchedUpdates`.
    
  184.       ReactNoop.batchedUpdates(() => {
    
  185.         root.render(<Text text="A" />);
    
  186.         root.render(<Text text="B" />);
    
  187.       });
    
  188. 
    
  189.       await null;
    
  190.       assertLog([
    
  191.         // A and B should render in a single batch _before_ the microtask queue
    
  192.         // has run. This replicates the behavior of the original `act`
    
  193.         // implementation, for compatibility.
    
  194.         'B',
    
  195.         'Current tree in microtask: B',
    
  196. 
    
  197.         // C isn't scheduled until a microtask, so it's rendered separately.
    
  198.         'C',
    
  199.       ]);
    
  200. 
    
  201.       // Subsequent updates should also render in separate batches.
    
  202.       root.render(<Text text="D" />);
    
  203.       root.render(<Text text="E" />);
    
  204.       assertLog(['D', 'E']);
    
  205.     });
    
  206.   });
    
  207. 
    
  208.   // @gate __DEV__
    
  209.   test('unwraps promises by yielding to microtasks (async act scope)', async () => {
    
  210.     const promise = Promise.resolve('Async');
    
  211. 
    
  212.     function Fallback() {
    
  213.       throw new Error('Fallback should never be rendered');
    
  214.     }
    
  215. 
    
  216.     function App() {
    
  217.       return use(promise);
    
  218.     }
    
  219. 
    
  220.     const root = ReactNoop.createRoot();
    
  221.     await act(async () => {
    
  222.       startTransition(() => {
    
  223.         root.render(
    
  224.           <Suspense fallback={<Fallback />}>
    
  225.             <App />
    
  226.           </Suspense>,
    
  227.         );
    
  228.       });
    
  229.     });
    
  230.     expect(root).toMatchRenderedOutput('Async');
    
  231.   });
    
  232. 
    
  233.   // @gate __DEV__
    
  234.   test('unwraps promises by yielding to microtasks (non-async act scope)', async () => {
    
  235.     const promise = Promise.resolve('Async');
    
  236. 
    
  237.     function Fallback() {
    
  238.       throw new Error('Fallback should never be rendered');
    
  239.     }
    
  240. 
    
  241.     function App() {
    
  242.       return use(promise);
    
  243.     }
    
  244. 
    
  245.     const root = ReactNoop.createRoot();
    
  246. 
    
  247.     // Note that the scope function is not an async function
    
  248.     await act(() => {
    
  249.       startTransition(() => {
    
  250.         root.render(
    
  251.           <Suspense fallback={<Fallback />}>
    
  252.             <App />
    
  253.           </Suspense>,
    
  254.         );
    
  255.       });
    
  256.     });
    
  257.     expect(root).toMatchRenderedOutput('Async');
    
  258.   });
    
  259. 
    
  260.   // @gate __DEV__
    
  261.   test('warns if a promise is used in a non-awaited `act` scope', async () => {
    
  262.     const promise = new Promise(() => {});
    
  263. 
    
  264.     function Fallback() {
    
  265.       throw new Error('Fallback should never be rendered');
    
  266.     }
    
  267. 
    
  268.     function App() {
    
  269.       return use(promise);
    
  270.     }
    
  271. 
    
  272.     spyOnDev(console, 'error').mockImplementation(() => {});
    
  273.     const root = ReactNoop.createRoot();
    
  274.     act(() => {
    
  275.       startTransition(() => {
    
  276.         root.render(
    
  277.           <Suspense fallback={<Fallback />}>
    
  278.             <App />
    
  279.           </Suspense>,
    
  280.         );
    
  281.       });
    
  282.     });
    
  283. 
    
  284.     // `act` warns after a few microtasks, instead of a macrotask, so that it's
    
  285.     // more likely to be attributed to the correct test case.
    
  286.     //
    
  287.     // The exact number of microtasks is an implementation detail; just needs
    
  288.     // to happen when the microtask queue is flushed.
    
  289.     await waitForMicrotasks();
    
  290. 
    
  291.     expect(console.error).toHaveBeenCalledTimes(1);
    
  292.     expect(console.error.mock.calls[0][0]).toContain(
    
  293.       'Warning: A component suspended inside an `act` scope, but the `act` ' +
    
  294.         'call was not awaited. When testing React components that ' +
    
  295.         'depend on asynchronous data, you must await the result:\n\n' +
    
  296.         'await act(() => ...)',
    
  297.     );
    
  298.   });
    
  299. 
    
  300.   // @gate __DEV__
    
  301.   test('does not warn when suspending via legacy `throw` API  in non-awaited `act` scope', async () => {
    
  302.     let didResolve = false;
    
  303.     let resolvePromise;
    
  304.     const promise = new Promise(r => {
    
  305.       resolvePromise = () => {
    
  306.         didResolve = true;
    
  307.         r();
    
  308.       };
    
  309.     });
    
  310. 
    
  311.     function Fallback() {
    
  312.       return 'Loading...';
    
  313.     }
    
  314. 
    
  315.     function App() {
    
  316.       if (!didResolve) {
    
  317.         throw promise;
    
  318.       }
    
  319.       return 'Async';
    
  320.     }
    
  321. 
    
  322.     spyOnDev(console, 'error').mockImplementation(() => {});
    
  323.     const root = ReactNoop.createRoot();
    
  324.     act(() => {
    
  325.       startTransition(() => {
    
  326.         root.render(
    
  327.           <Suspense fallback={<Fallback />}>
    
  328.             <App />
    
  329.           </Suspense>,
    
  330.         );
    
  331.       });
    
  332.     });
    
  333.     expect(root).toMatchRenderedOutput('Loading...');
    
  334. 
    
  335.     // `act` warns after a few microtasks, instead of a macrotask, so that it's
    
  336.     // more likely to be attributed to the correct test case.
    
  337.     //
    
  338.     // The exact number of microtasks is an implementation detail; just needs
    
  339.     // to happen when the microtask queue is flushed.
    
  340.     await waitForMicrotasks();
    
  341. 
    
  342.     expect(console.error).toHaveBeenCalledTimes(0);
    
  343. 
    
  344.     // Finish loading the data
    
  345.     await act(async () => {
    
  346.       resolvePromise();
    
  347.     });
    
  348.     expect(root).toMatchRenderedOutput('Async');
    
  349.   });
    
  350. });