1. let React;
    
  2. let ReactNoop;
    
  3. let Scheduler;
    
  4. let act;
    
  5. let useState;
    
  6. let useEffect;
    
  7. let startTransition;
    
  8. let assertLog;
    
  9. let waitForPaint;
    
  10. 
    
  11. // TODO: Migrate tests to React DOM instead of React Noop
    
  12. 
    
  13. describe('ReactFlushSync', () => {
    
  14.   beforeEach(() => {
    
  15.     jest.resetModules();
    
  16. 
    
  17.     React = require('react');
    
  18.     ReactNoop = require('react-noop-renderer');
    
  19.     Scheduler = require('scheduler');
    
  20.     act = require('internal-test-utils').act;
    
  21.     useState = React.useState;
    
  22.     useEffect = React.useEffect;
    
  23.     startTransition = React.startTransition;
    
  24. 
    
  25.     const InternalTestUtils = require('internal-test-utils');
    
  26.     assertLog = InternalTestUtils.assertLog;
    
  27.     waitForPaint = InternalTestUtils.waitForPaint;
    
  28.   });
    
  29. 
    
  30.   function Text({text}) {
    
  31.     Scheduler.log(text);
    
  32.     return text;
    
  33.   }
    
  34. 
    
  35.   test('changes priority of updates in useEffect', async () => {
    
  36.     spyOnDev(console, 'error').mockImplementation(() => {});
    
  37. 
    
  38.     function App() {
    
  39.       const [syncState, setSyncState] = useState(0);
    
  40.       const [state, setState] = useState(0);
    
  41.       useEffect(() => {
    
  42.         if (syncState !== 1) {
    
  43.           setState(1);
    
  44.           ReactNoop.flushSync(() => setSyncState(1));
    
  45.         }
    
  46.       }, [syncState, state]);
    
  47.       return <Text text={`${syncState}, ${state}`} />;
    
  48.     }
    
  49. 
    
  50.     const root = ReactNoop.createRoot();
    
  51.     await act(async () => {
    
  52.       React.startTransition(() => {
    
  53.         root.render(<App />);
    
  54.       });
    
  55.       // This will yield right before the passive effect fires
    
  56.       await waitForPaint(['0, 0']);
    
  57. 
    
  58.       // The passive effect will schedule a sync update and a normal update.
    
  59.       // They should commit in two separate batches. First the sync one.
    
  60.       await waitForPaint(
    
  61.         gate(flags => flags.enableUnifiedSyncLane) ? ['1, 1'] : ['1, 0'],
    
  62.       );
    
  63. 
    
  64.       // The remaining update is not sync
    
  65.       ReactNoop.flushSync();
    
  66.       assertLog([]);
    
  67. 
    
  68.       if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  69.         await waitForPaint([]);
    
  70.       } else {
    
  71.         // Now flush it.
    
  72.         await waitForPaint(['1, 1']);
    
  73.       }
    
  74.     });
    
  75.     expect(root).toMatchRenderedOutput('1, 1');
    
  76. 
    
  77.     if (__DEV__) {
    
  78.       expect(console.error.mock.calls[0][0]).toContain(
    
  79.         'flushSync was called from inside a lifecycle method. React ' +
    
  80.           'cannot flush when React is already rendering. Consider moving this ' +
    
  81.           'call to a scheduler task or micro task.%s',
    
  82.       );
    
  83.     }
    
  84.   });
    
  85. 
    
  86.   test('nested with startTransition', async () => {
    
  87.     let setSyncState;
    
  88.     let setState;
    
  89.     function App() {
    
  90.       const [syncState, _setSyncState] = useState(0);
    
  91.       const [state, _setState] = useState(0);
    
  92.       setSyncState = _setSyncState;
    
  93.       setState = _setState;
    
  94.       return <Text text={`${syncState}, ${state}`} />;
    
  95.     }
    
  96. 
    
  97.     const root = ReactNoop.createRoot();
    
  98.     await act(() => {
    
  99.       root.render(<App />);
    
  100.     });
    
  101.     assertLog(['0, 0']);
    
  102.     expect(root).toMatchRenderedOutput('0, 0');
    
  103. 
    
  104.     await act(() => {
    
  105.       ReactNoop.flushSync(() => {
    
  106.         startTransition(() => {
    
  107.           // This should be async even though flushSync is on the stack, because
    
  108.           // startTransition is closer.
    
  109.           setState(1);
    
  110.           ReactNoop.flushSync(() => {
    
  111.             // This should be async even though startTransition is on the stack,
    
  112.             // because flushSync is closer.
    
  113.             setSyncState(1);
    
  114.           });
    
  115.         });
    
  116.       });
    
  117.       // Only the sync update should have flushed
    
  118.       assertLog(['1, 0']);
    
  119.       expect(root).toMatchRenderedOutput('1, 0');
    
  120.     });
    
  121.     // Now the async update has flushed, too.
    
  122.     assertLog(['1, 1']);
    
  123.     expect(root).toMatchRenderedOutput('1, 1');
    
  124.   });
    
  125. 
    
  126.   test('flushes passive effects synchronously when they are the result of a sync render', async () => {
    
  127.     function App() {
    
  128.       useEffect(() => {
    
  129.         Scheduler.log('Effect');
    
  130.       }, []);
    
  131.       return <Text text="Child" />;
    
  132.     }
    
  133. 
    
  134.     const root = ReactNoop.createRoot();
    
  135.     await act(() => {
    
  136.       ReactNoop.flushSync(() => {
    
  137.         root.render(<App />);
    
  138.       });
    
  139.       assertLog([
    
  140.         'Child',
    
  141.         // Because the pending effect was the result of a sync update, calling
    
  142.         // flushSync should flush it.
    
  143.         'Effect',
    
  144.       ]);
    
  145.       expect(root).toMatchRenderedOutput('Child');
    
  146.     });
    
  147.   });
    
  148. 
    
  149.   test('do not flush passive effects synchronously after render in legacy mode', async () => {
    
  150.     function App() {
    
  151.       useEffect(() => {
    
  152.         Scheduler.log('Effect');
    
  153.       }, []);
    
  154.       return <Text text="Child" />;
    
  155.     }
    
  156. 
    
  157.     const root = ReactNoop.createLegacyRoot();
    
  158.     await act(() => {
    
  159.       ReactNoop.flushSync(() => {
    
  160.         root.render(<App />);
    
  161.       });
    
  162.       assertLog([
    
  163.         'Child',
    
  164.         // Because we're in legacy mode, we shouldn't have flushed the passive
    
  165.         // effects yet.
    
  166.       ]);
    
  167.       expect(root).toMatchRenderedOutput('Child');
    
  168.     });
    
  169.     // Effect flushes after paint.
    
  170.     assertLog(['Effect']);
    
  171.   });
    
  172. 
    
  173.   test('flush pending passive effects before scope is called in legacy mode', async () => {
    
  174.     let currentStep = 0;
    
  175. 
    
  176.     function App({step}) {
    
  177.       useEffect(() => {
    
  178.         currentStep = step;
    
  179.         Scheduler.log('Effect: ' + step);
    
  180.       }, [step]);
    
  181.       return <Text text={step} />;
    
  182.     }
    
  183. 
    
  184.     const root = ReactNoop.createLegacyRoot();
    
  185.     await act(() => {
    
  186.       ReactNoop.flushSync(() => {
    
  187.         root.render(<App step={1} />);
    
  188.       });
    
  189.       assertLog([
    
  190.         1,
    
  191.         // Because we're in legacy mode, we shouldn't have flushed the passive
    
  192.         // effects yet.
    
  193.       ]);
    
  194.       expect(root).toMatchRenderedOutput('1');
    
  195. 
    
  196.       ReactNoop.flushSync(() => {
    
  197.         // This should render step 2 because the passive effect has already
    
  198.         // fired, before the scope function is called.
    
  199.         root.render(<App step={currentStep + 1} />);
    
  200.       });
    
  201.       assertLog(['Effect: 1', 2]);
    
  202.       expect(root).toMatchRenderedOutput('2');
    
  203.     });
    
  204.     assertLog(['Effect: 2']);
    
  205.   });
    
  206. 
    
  207.   test("do not flush passive effects synchronously when they aren't the result of a sync render", async () => {
    
  208.     function App() {
    
  209.       useEffect(() => {
    
  210.         Scheduler.log('Effect');
    
  211.       }, []);
    
  212.       return <Text text="Child" />;
    
  213.     }
    
  214. 
    
  215.     const root = ReactNoop.createRoot();
    
  216.     await act(async () => {
    
  217.       root.render(<App />);
    
  218.       await waitForPaint([
    
  219.         'Child',
    
  220.         // Because the passive effect was not the result of a sync update, it
    
  221.         // should not flush before paint.
    
  222.       ]);
    
  223.       expect(root).toMatchRenderedOutput('Child');
    
  224.     });
    
  225.     // Effect flushes after paint.
    
  226.     assertLog(['Effect']);
    
  227.   });
    
  228. 
    
  229.   test('does not flush pending passive effects', async () => {
    
  230.     function App() {
    
  231.       useEffect(() => {
    
  232.         Scheduler.log('Effect');
    
  233.       }, []);
    
  234.       return <Text text="Child" />;
    
  235.     }
    
  236. 
    
  237.     const root = ReactNoop.createRoot();
    
  238.     await act(async () => {
    
  239.       root.render(<App />);
    
  240.       await waitForPaint(['Child']);
    
  241.       expect(root).toMatchRenderedOutput('Child');
    
  242. 
    
  243.       // Passive effects are pending. Calling flushSync should not affect them.
    
  244.       ReactNoop.flushSync();
    
  245.       // Effects still haven't fired.
    
  246.       assertLog([]);
    
  247.     });
    
  248.     // Now the effects have fired.
    
  249.     assertLog(['Effect']);
    
  250.   });
    
  251. 
    
  252.   test('completely exhausts synchronous work queue even if something throws', async () => {
    
  253.     function Throws({error}) {
    
  254.       throw error;
    
  255.     }
    
  256. 
    
  257.     const root1 = ReactNoop.createRoot();
    
  258.     const root2 = ReactNoop.createRoot();
    
  259.     const root3 = ReactNoop.createRoot();
    
  260. 
    
  261.     await act(async () => {
    
  262.       root1.render(<Text text="Hi" />);
    
  263.       root2.render(<Text text="Andrew" />);
    
  264.       root3.render(<Text text="!" />);
    
  265.     });
    
  266.     assertLog(['Hi', 'Andrew', '!']);
    
  267. 
    
  268.     const aahh = new Error('AAHH!');
    
  269.     const nooo = new Error('Noooooooooo!');
    
  270. 
    
  271.     let error;
    
  272.     try {
    
  273.       ReactNoop.flushSync(() => {
    
  274.         root1.render(<Throws error={aahh} />);
    
  275.         root2.render(<Throws error={nooo} />);
    
  276.         root3.render(<Text text="aww" />);
    
  277.       });
    
  278.     } catch (e) {
    
  279.       error = e;
    
  280.     }
    
  281. 
    
  282.     // The update to root 3 should have finished synchronously, even though the
    
  283.     // earlier updates errored.
    
  284.     assertLog(['aww']);
    
  285.     // Roots 1 and 2 were unmounted.
    
  286.     expect(root1).toMatchRenderedOutput(null);
    
  287.     expect(root2).toMatchRenderedOutput(null);
    
  288.     expect(root3).toMatchRenderedOutput('aww');
    
  289. 
    
  290.     // Because there were multiple errors, React threw an AggregateError.
    
  291.     // eslint-disable-next-line no-undef
    
  292.     expect(error).toBeInstanceOf(AggregateError);
    
  293.     expect(error.errors.length).toBe(2);
    
  294.     expect(error.errors[0]).toBe(aahh);
    
  295.     expect(error.errors[1]).toBe(nooo);
    
  296.   });
    
  297. });