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. let React;
    
  11. let Scheduler;
    
  12. let waitForAll;
    
  13. let assertLog;
    
  14. let ReactNoop;
    
  15. let useState;
    
  16. let act;
    
  17. let Suspense;
    
  18. let startTransition;
    
  19. let getCacheForType;
    
  20. let caches;
    
  21. 
    
  22. // These tests are mostly concerned with concurrent roots. The legacy root
    
  23. // behavior is covered by other older test suites and is unchanged from
    
  24. // React 17.
    
  25. describe('act warnings', () => {
    
  26.   beforeEach(() => {
    
  27.     jest.resetModules();
    
  28.     React = require('react');
    
  29.     Scheduler = require('scheduler');
    
  30.     ReactNoop = require('react-noop-renderer');
    
  31.     act = React.unstable_act;
    
  32.     useState = React.useState;
    
  33.     Suspense = React.Suspense;
    
  34.     startTransition = React.startTransition;
    
  35.     getCacheForType = React.unstable_getCacheForType;
    
  36.     caches = [];
    
  37. 
    
  38.     const InternalTestUtils = require('internal-test-utils');
    
  39.     waitForAll = InternalTestUtils.waitForAll;
    
  40.     assertLog = InternalTestUtils.assertLog;
    
  41.   });
    
  42. 
    
  43.   function createTextCache() {
    
  44.     const data = new Map();
    
  45.     const version = caches.length + 1;
    
  46.     const cache = {
    
  47.       version,
    
  48.       data,
    
  49.       resolve(text) {
    
  50.         const record = data.get(text);
    
  51.         if (record === undefined) {
    
  52.           const newRecord = {
    
  53.             status: 'resolved',
    
  54.             value: text,
    
  55.           };
    
  56.           data.set(text, newRecord);
    
  57.         } else if (record.status === 'pending') {
    
  58.           const thenable = record.value;
    
  59.           record.status = 'resolved';
    
  60.           record.value = text;
    
  61.           thenable.pings.forEach(t => t());
    
  62.         }
    
  63.       },
    
  64.       reject(text, error) {
    
  65.         const record = data.get(text);
    
  66.         if (record === undefined) {
    
  67.           const newRecord = {
    
  68.             status: 'rejected',
    
  69.             value: error,
    
  70.           };
    
  71.           data.set(text, newRecord);
    
  72.         } else if (record.status === 'pending') {
    
  73.           const thenable = record.value;
    
  74.           record.status = 'rejected';
    
  75.           record.value = error;
    
  76.           thenable.pings.forEach(t => t());
    
  77.         }
    
  78.       },
    
  79.     };
    
  80.     caches.push(cache);
    
  81.     return cache;
    
  82.   }
    
  83. 
    
  84.   function readText(text) {
    
  85.     const textCache = getCacheForType(createTextCache);
    
  86.     const record = textCache.data.get(text);
    
  87.     if (record !== undefined) {
    
  88.       switch (record.status) {
    
  89.         case 'pending':
    
  90.           Scheduler.log(`Suspend! [${text}]`);
    
  91.           throw record.value;
    
  92.         case 'rejected':
    
  93.           Scheduler.log(`Error! [${text}]`);
    
  94.           throw record.value;
    
  95.         case 'resolved':
    
  96.           return textCache.version;
    
  97.       }
    
  98.     } else {
    
  99.       Scheduler.log(`Suspend! [${text}]`);
    
  100. 
    
  101.       const thenable = {
    
  102.         pings: [],
    
  103.         then(resolve) {
    
  104.           if (newRecord.status === 'pending') {
    
  105.             thenable.pings.push(resolve);
    
  106.           } else {
    
  107.             Promise.resolve().then(() => resolve(newRecord.value));
    
  108.           }
    
  109.         },
    
  110.       };
    
  111. 
    
  112.       const newRecord = {
    
  113.         status: 'pending',
    
  114.         value: thenable,
    
  115.       };
    
  116.       textCache.data.set(text, newRecord);
    
  117. 
    
  118.       throw thenable;
    
  119.     }
    
  120.   }
    
  121. 
    
  122.   function Text({text}) {
    
  123.     Scheduler.log(text);
    
  124.     return text;
    
  125.   }
    
  126. 
    
  127.   function AsyncText({text}) {
    
  128.     readText(text);
    
  129.     Scheduler.log(text);
    
  130.     return text;
    
  131.   }
    
  132. 
    
  133.   function resolveText(text) {
    
  134.     if (caches.length === 0) {
    
  135.       throw Error('Cache does not exist.');
    
  136.     } else {
    
  137.       // Resolve the most recently created cache. An older cache can by
    
  138.       // resolved with `caches[index].resolve(text)`.
    
  139.       caches[caches.length - 1].resolve(text);
    
  140.     }
    
  141.   }
    
  142. 
    
  143.   async function withActEnvironment(value, scope) {
    
  144.     const prevValue = global.IS_REACT_ACT_ENVIRONMENT;
    
  145.     global.IS_REACT_ACT_ENVIRONMENT = value;
    
  146.     try {
    
  147.       return await scope();
    
  148.     } finally {
    
  149.       global.IS_REACT_ACT_ENVIRONMENT = prevValue;
    
  150.     }
    
  151.   }
    
  152. 
    
  153.   test('warns about unwrapped updates only if environment flag is enabled', async () => {
    
  154.     let setState;
    
  155.     function App() {
    
  156.       const [state, _setState] = useState(0);
    
  157.       setState = _setState;
    
  158.       return <Text text={state} />;
    
  159.     }
    
  160. 
    
  161.     const root = ReactNoop.createRoot();
    
  162.     root.render(<App />);
    
  163.     await waitForAll([0]);
    
  164.     expect(root).toMatchRenderedOutput('0');
    
  165. 
    
  166.     // Default behavior. Flag is undefined. No warning.
    
  167.     expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
    
  168.     setState(1);
    
  169.     await waitForAll([1]);
    
  170.     expect(root).toMatchRenderedOutput('1');
    
  171. 
    
  172.     // Flag is true. Warn.
    
  173.     await withActEnvironment(true, async () => {
    
  174.       expect(() => setState(2)).toErrorDev(
    
  175.         'An update to App inside a test was not wrapped in act',
    
  176.       );
    
  177.       await waitForAll([2]);
    
  178.       expect(root).toMatchRenderedOutput('2');
    
  179.     });
    
  180. 
    
  181.     // Flag is false. No warning.
    
  182.     await withActEnvironment(false, async () => {
    
  183.       setState(3);
    
  184.       await waitForAll([3]);
    
  185.       expect(root).toMatchRenderedOutput('3');
    
  186.     });
    
  187.   });
    
  188. 
    
  189.   // @gate __DEV__
    
  190.   test('act warns if the environment flag is not enabled', async () => {
    
  191.     let setState;
    
  192.     function App() {
    
  193.       const [state, _setState] = useState(0);
    
  194.       setState = _setState;
    
  195.       return <Text text={state} />;
    
  196.     }
    
  197. 
    
  198.     const root = ReactNoop.createRoot();
    
  199.     root.render(<App />);
    
  200.     await waitForAll([0]);
    
  201.     expect(root).toMatchRenderedOutput('0');
    
  202. 
    
  203.     // Default behavior. Flag is undefined. Warn.
    
  204.     expect(global.IS_REACT_ACT_ENVIRONMENT).toBe(undefined);
    
  205.     expect(() => {
    
  206.       act(() => {
    
  207.         setState(1);
    
  208.       });
    
  209.     }).toErrorDev(
    
  210.       'The current testing environment is not configured to support act(...)',
    
  211.       {withoutStack: true},
    
  212.     );
    
  213.     assertLog([1]);
    
  214.     expect(root).toMatchRenderedOutput('1');
    
  215. 
    
  216.     // Flag is true. Don't warn.
    
  217.     await withActEnvironment(true, () => {
    
  218.       act(() => {
    
  219.         setState(2);
    
  220.       });
    
  221.       assertLog([2]);
    
  222.       expect(root).toMatchRenderedOutput('2');
    
  223.     });
    
  224. 
    
  225.     // Flag is false. Warn.
    
  226.     await withActEnvironment(false, () => {
    
  227.       expect(() => {
    
  228.         act(() => {
    
  229.           setState(1);
    
  230.         });
    
  231.       }).toErrorDev(
    
  232.         'The current testing environment is not configured to support act(...)',
    
  233.         {withoutStack: true},
    
  234.       );
    
  235.       assertLog([1]);
    
  236.       expect(root).toMatchRenderedOutput('1');
    
  237.     });
    
  238.   });
    
  239. 
    
  240.   test('warns if root update is not wrapped', async () => {
    
  241.     await withActEnvironment(true, () => {
    
  242.       const root = ReactNoop.createRoot();
    
  243.       expect(() => root.render('Hi')).toErrorDev(
    
  244.         // TODO: Better error message that doesn't make it look like "Root" is
    
  245.         // the name of a custom component
    
  246.         'An update to Root inside a test was not wrapped in act(...)',
    
  247.         {withoutStack: true},
    
  248.       );
    
  249.     });
    
  250.   });
    
  251. 
    
  252.   // @gate __DEV__
    
  253.   test('warns if class update is not wrapped', async () => {
    
  254.     let app;
    
  255.     class App extends React.Component {
    
  256.       state = {count: 0};
    
  257.       render() {
    
  258.         app = this;
    
  259.         return <Text text={this.state.count} />;
    
  260.       }
    
  261.     }
    
  262. 
    
  263.     await withActEnvironment(true, () => {
    
  264.       const root = ReactNoop.createRoot();
    
  265.       act(() => {
    
  266.         root.render(<App />);
    
  267.       });
    
  268.       expect(() => app.setState({count: 1})).toErrorDev(
    
  269.         'An update to App inside a test was not wrapped in act(...)',
    
  270.       );
    
  271.     });
    
  272.   });
    
  273. 
    
  274.   // @gate __DEV__
    
  275.   test('warns even if update is synchronous', async () => {
    
  276.     let setState;
    
  277.     function App() {
    
  278.       const [state, _setState] = useState(0);
    
  279.       setState = _setState;
    
  280.       return <Text text={state} />;
    
  281.     }
    
  282. 
    
  283.     await withActEnvironment(true, () => {
    
  284.       const root = ReactNoop.createRoot();
    
  285.       act(() => root.render(<App />));
    
  286.       assertLog([0]);
    
  287.       expect(root).toMatchRenderedOutput('0');
    
  288. 
    
  289.       // Even though this update is synchronous, we should still fire a warning,
    
  290.       // because it could have spawned additional asynchronous work
    
  291.       expect(() => ReactNoop.flushSync(() => setState(1))).toErrorDev(
    
  292.         'An update to App inside a test was not wrapped in act(...)',
    
  293.       );
    
  294. 
    
  295.       assertLog([1]);
    
  296.       expect(root).toMatchRenderedOutput('1');
    
  297.     });
    
  298.   });
    
  299. 
    
  300.   // @gate __DEV__
    
  301.   // @gate enableLegacyCache
    
  302.   test('warns if Suspense retry is not wrapped', async () => {
    
  303.     function App() {
    
  304.       return (
    
  305.         <Suspense fallback={<Text text="Loading..." />}>
    
  306.           <AsyncText text="Async" />
    
  307.         </Suspense>
    
  308.       );
    
  309.     }
    
  310. 
    
  311.     await withActEnvironment(true, () => {
    
  312.       const root = ReactNoop.createRoot();
    
  313.       act(() => {
    
  314.         root.render(<App />);
    
  315.       });
    
  316.       assertLog(['Suspend! [Async]', 'Loading...']);
    
  317.       expect(root).toMatchRenderedOutput('Loading...');
    
  318. 
    
  319.       // This is a retry, not a ping, because we already showed a fallback.
    
  320.       expect(() => resolveText('Async')).toErrorDev(
    
  321.         'A suspended resource finished loading inside a test, but the event ' +
    
  322.           'was not wrapped in act(...)',
    
  323.         {withoutStack: true},
    
  324.       );
    
  325.     });
    
  326.   });
    
  327. 
    
  328.   // @gate __DEV__
    
  329.   // @gate enableLegacyCache
    
  330.   test('warns if Suspense ping is not wrapped', async () => {
    
  331.     function App({showMore}) {
    
  332.       return (
    
  333.         <Suspense fallback={<Text text="Loading..." />}>
    
  334.           {showMore ? <AsyncText text="Async" /> : <Text text="(empty)" />}
    
  335.         </Suspense>
    
  336.       );
    
  337.     }
    
  338. 
    
  339.     await withActEnvironment(true, () => {
    
  340.       const root = ReactNoop.createRoot();
    
  341.       act(() => {
    
  342.         root.render(<App showMore={false} />);
    
  343.       });
    
  344.       assertLog(['(empty)']);
    
  345.       expect(root).toMatchRenderedOutput('(empty)');
    
  346. 
    
  347.       act(() => {
    
  348.         startTransition(() => {
    
  349.           root.render(<App showMore={true} />);
    
  350.         });
    
  351.       });
    
  352.       assertLog(['Suspend! [Async]', 'Loading...']);
    
  353.       expect(root).toMatchRenderedOutput('(empty)');
    
  354. 
    
  355.       // This is a ping, not a retry, because no fallback is showing.
    
  356.       expect(() => resolveText('Async')).toErrorDev(
    
  357.         'A suspended resource finished loading inside a test, but the event ' +
    
  358.           'was not wrapped in act(...)',
    
  359.         {withoutStack: true},
    
  360.       );
    
  361.     });
    
  362.   });
    
  363. });