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.  * @emails react-core
    
  8.  * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
    
  9.  */
    
  10. 
    
  11. /* eslint-disable no-func-assign */
    
  12. 
    
  13. 'use strict';
    
  14. 
    
  15. const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
    
  16. 
    
  17. let React;
    
  18. let ReactDOM;
    
  19. let ReactDOMServer;
    
  20. let ReactTestUtils;
    
  21. let useState;
    
  22. let useReducer;
    
  23. let useEffect;
    
  24. let useContext;
    
  25. let useCallback;
    
  26. let useMemo;
    
  27. let useRef;
    
  28. let useImperativeHandle;
    
  29. let useInsertionEffect;
    
  30. let useLayoutEffect;
    
  31. let useDebugValue;
    
  32. let forwardRef;
    
  33. let yieldedValues;
    
  34. let yieldValue;
    
  35. let clearLog;
    
  36. 
    
  37. function initModules() {
    
  38.   // Reset warning cache.
    
  39.   jest.resetModules();
    
  40. 
    
  41.   React = require('react');
    
  42.   ReactDOM = require('react-dom');
    
  43.   ReactDOMServer = require('react-dom/server');
    
  44.   ReactTestUtils = require('react-dom/test-utils');
    
  45.   useState = React.useState;
    
  46.   useReducer = React.useReducer;
    
  47.   useEffect = React.useEffect;
    
  48.   useContext = React.useContext;
    
  49.   useCallback = React.useCallback;
    
  50.   useMemo = React.useMemo;
    
  51.   useRef = React.useRef;
    
  52.   useDebugValue = React.useDebugValue;
    
  53.   useImperativeHandle = React.useImperativeHandle;
    
  54.   useInsertionEffect = React.useInsertionEffect;
    
  55.   useLayoutEffect = React.useLayoutEffect;
    
  56.   forwardRef = React.forwardRef;
    
  57. 
    
  58.   yieldedValues = [];
    
  59.   yieldValue = value => {
    
  60.     yieldedValues.push(value);
    
  61.   };
    
  62.   clearLog = () => {
    
  63.     const ret = yieldedValues;
    
  64.     yieldedValues = [];
    
  65.     return ret;
    
  66.   };
    
  67. 
    
  68.   // Make them available to the helpers.
    
  69.   return {
    
  70.     ReactDOM,
    
  71.     ReactDOMServer,
    
  72.     ReactTestUtils,
    
  73.   };
    
  74. }
    
  75. 
    
  76. const {resetModules, itRenders, itThrowsWhenRendering, serverRender} =
    
  77.   ReactDOMServerIntegrationUtils(initModules);
    
  78. 
    
  79. describe('ReactDOMServerHooks', () => {
    
  80.   beforeEach(() => {
    
  81.     resetModules();
    
  82.   });
    
  83. 
    
  84.   function Text(props) {
    
  85.     yieldValue(props.text);
    
  86.     return <span>{props.text}</span>;
    
  87.   }
    
  88. 
    
  89.   describe('useState', () => {
    
  90.     itRenders('basic render', async render => {
    
  91.       function Counter(props) {
    
  92.         const [count] = useState(0);
    
  93.         return <span>Count: {count}</span>;
    
  94.       }
    
  95. 
    
  96.       const domNode = await render(<Counter />);
    
  97.       expect(domNode.textContent).toEqual('Count: 0');
    
  98.     });
    
  99. 
    
  100.     itRenders('lazy state initialization', async render => {
    
  101.       function Counter(props) {
    
  102.         const [count] = useState(() => {
    
  103.           return 0;
    
  104.         });
    
  105.         return <span>Count: {count}</span>;
    
  106.       }
    
  107. 
    
  108.       const domNode = await render(<Counter />);
    
  109.       expect(domNode.textContent).toEqual('Count: 0');
    
  110.     });
    
  111. 
    
  112.     it('does not trigger a re-renders when updater is invoked outside current render function', async () => {
    
  113.       function UpdateCount({setCount, count, children}) {
    
  114.         if (count < 3) {
    
  115.           setCount(c => c + 1);
    
  116.         }
    
  117.         return <span>{children}</span>;
    
  118.       }
    
  119.       function Counter() {
    
  120.         const [count, setCount] = useState(0);
    
  121.         return (
    
  122.           <div>
    
  123.             <UpdateCount setCount={setCount} count={count}>
    
  124.               Count: {count}
    
  125.             </UpdateCount>
    
  126.           </div>
    
  127.         );
    
  128.       }
    
  129. 
    
  130.       const domNode = await serverRender(<Counter />);
    
  131.       expect(domNode.textContent).toEqual('Count: 0');
    
  132.     });
    
  133. 
    
  134.     itThrowsWhenRendering(
    
  135.       'if used inside a class component',
    
  136.       async render => {
    
  137.         class Counter extends React.Component {
    
  138.           render() {
    
  139.             const [count] = useState(0);
    
  140.             return <Text text={count} />;
    
  141.           }
    
  142.         }
    
  143. 
    
  144.         return render(<Counter />);
    
  145.       },
    
  146.       'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    
  147.         ' one of the following reasons:\n' +
    
  148.         '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    
  149.         '2. You might be breaking the Rules of Hooks\n' +
    
  150.         '3. You might have more than one copy of React in the same app\n' +
    
  151.         'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    
  152.     );
    
  153. 
    
  154.     itRenders('multiple times when an updater is called', async render => {
    
  155.       function Counter() {
    
  156.         const [count, setCount] = useState(0);
    
  157.         if (count < 12) {
    
  158.           setCount(c => c + 1);
    
  159.           setCount(c => c + 1);
    
  160.           setCount(c => c + 1);
    
  161.         }
    
  162.         return <Text text={'Count: ' + count} />;
    
  163.       }
    
  164. 
    
  165.       const domNode = await render(<Counter />);
    
  166.       expect(domNode.textContent).toEqual('Count: 12');
    
  167.     });
    
  168. 
    
  169.     itRenders('until there are no more new updates', async render => {
    
  170.       function Counter() {
    
  171.         const [count, setCount] = useState(0);
    
  172.         if (count < 3) {
    
  173.           setCount(count + 1);
    
  174.         }
    
  175.         return <span>Count: {count}</span>;
    
  176.       }
    
  177. 
    
  178.       const domNode = await render(<Counter />);
    
  179.       expect(domNode.textContent).toEqual('Count: 3');
    
  180.     });
    
  181. 
    
  182.     itThrowsWhenRendering(
    
  183.       'after too many iterations',
    
  184.       async render => {
    
  185.         function Counter() {
    
  186.           const [count, setCount] = useState(0);
    
  187.           setCount(count + 1);
    
  188.           return <span>{count}</span>;
    
  189.         }
    
  190.         return render(<Counter />);
    
  191.       },
    
  192.       'Too many re-renders. React limits the number of renders to prevent ' +
    
  193.         'an infinite loop.',
    
  194.     );
    
  195.   });
    
  196. 
    
  197.   describe('useReducer', () => {
    
  198.     itRenders('with initial state', async render => {
    
  199.       function reducer(state, action) {
    
  200.         return action === 'increment' ? state + 1 : state;
    
  201.       }
    
  202.       function Counter() {
    
  203.         const [count] = useReducer(reducer, 0);
    
  204.         yieldValue('Render: ' + count);
    
  205.         return <Text text={count} />;
    
  206.       }
    
  207. 
    
  208.       const domNode = await render(<Counter />);
    
  209. 
    
  210.       expect(clearLog()).toEqual(['Render: 0', 0]);
    
  211.       expect(domNode.tagName).toEqual('SPAN');
    
  212.       expect(domNode.textContent).toEqual('0');
    
  213.     });
    
  214. 
    
  215.     itRenders('lazy initialization', async render => {
    
  216.       function reducer(state, action) {
    
  217.         return action === 'increment' ? state + 1 : state;
    
  218.       }
    
  219.       function Counter() {
    
  220.         const [count] = useReducer(reducer, 0, c => c + 1);
    
  221.         yieldValue('Render: ' + count);
    
  222.         return <Text text={count} />;
    
  223.       }
    
  224. 
    
  225.       const domNode = await render(<Counter />);
    
  226. 
    
  227.       expect(clearLog()).toEqual(['Render: 1', 1]);
    
  228.       expect(domNode.tagName).toEqual('SPAN');
    
  229.       expect(domNode.textContent).toEqual('1');
    
  230.     });
    
  231. 
    
  232.     itRenders(
    
  233.       'multiple times when updates happen during the render phase',
    
  234.       async render => {
    
  235.         function reducer(state, action) {
    
  236.           return action === 'increment' ? state + 1 : state;
    
  237.         }
    
  238.         function Counter() {
    
  239.           const [count, dispatch] = useReducer(reducer, 0);
    
  240.           if (count < 3) {
    
  241.             dispatch('increment');
    
  242.           }
    
  243.           yieldValue('Render: ' + count);
    
  244.           return <Text text={count} />;
    
  245.         }
    
  246. 
    
  247.         const domNode = await render(<Counter />);
    
  248. 
    
  249.         expect(clearLog()).toEqual([
    
  250.           'Render: 0',
    
  251.           'Render: 1',
    
  252.           'Render: 2',
    
  253.           'Render: 3',
    
  254.           3,
    
  255.         ]);
    
  256.         expect(domNode.tagName).toEqual('SPAN');
    
  257.         expect(domNode.textContent).toEqual('3');
    
  258.       },
    
  259.     );
    
  260. 
    
  261.     itRenders(
    
  262.       'using reducer passed at time of render, not time of dispatch',
    
  263.       async render => {
    
  264.         // This test is a bit contrived but it demonstrates a subtle edge case.
    
  265. 
    
  266.         // Reducer A increments by 1. Reducer B increments by 10.
    
  267.         function reducerA(state, action) {
    
  268.           switch (action) {
    
  269.             case 'increment':
    
  270.               return state + 1;
    
  271.             case 'reset':
    
  272.               return 0;
    
  273.           }
    
  274.         }
    
  275.         function reducerB(state, action) {
    
  276.           switch (action) {
    
  277.             case 'increment':
    
  278.               return state + 10;
    
  279.             case 'reset':
    
  280.               return 0;
    
  281.           }
    
  282.         }
    
  283. 
    
  284.         function Counter() {
    
  285.           const [reducer, setReducer] = useState(() => reducerA);
    
  286.           const [count, dispatch] = useReducer(reducer, 0);
    
  287.           if (count < 20) {
    
  288.             dispatch('increment');
    
  289.             // Swap reducers each time we increment
    
  290.             if (reducer === reducerA) {
    
  291.               setReducer(() => reducerB);
    
  292.             } else {
    
  293.               setReducer(() => reducerA);
    
  294.             }
    
  295.           }
    
  296.           yieldValue('Render: ' + count);
    
  297.           return <Text text={count} />;
    
  298.         }
    
  299. 
    
  300.         const domNode = await render(<Counter />);
    
  301. 
    
  302.         expect(clearLog()).toEqual([
    
  303.           // The count should increase by alternating amounts of 10 and 1
    
  304.           // until we reach 21.
    
  305.           'Render: 0',
    
  306.           'Render: 10',
    
  307.           'Render: 11',
    
  308.           'Render: 21',
    
  309.           21,
    
  310.         ]);
    
  311.         expect(domNode.tagName).toEqual('SPAN');
    
  312.         expect(domNode.textContent).toEqual('21');
    
  313.       },
    
  314.     );
    
  315.   });
    
  316. 
    
  317.   describe('useMemo', () => {
    
  318.     itRenders('basic render', async render => {
    
  319.       function CapitalizedText(props) {
    
  320.         const text = props.text;
    
  321.         const capitalizedText = useMemo(() => {
    
  322.           yieldValue(`Capitalize '${text}'`);
    
  323.           return text.toUpperCase();
    
  324.         }, [text]);
    
  325.         return <Text text={capitalizedText} />;
    
  326.       }
    
  327. 
    
  328.       const domNode = await render(<CapitalizedText text="hello" />);
    
  329.       expect(clearLog()).toEqual(["Capitalize 'hello'", 'HELLO']);
    
  330.       expect(domNode.tagName).toEqual('SPAN');
    
  331.       expect(domNode.textContent).toEqual('HELLO');
    
  332.     });
    
  333. 
    
  334.     itRenders('if no inputs are provided', async render => {
    
  335.       function LazyCompute(props) {
    
  336.         const computed = useMemo(props.compute);
    
  337.         return <Text text={computed} />;
    
  338.       }
    
  339. 
    
  340.       function computeA() {
    
  341.         yieldValue('compute A');
    
  342.         return 'A';
    
  343.       }
    
  344. 
    
  345.       const domNode = await render(<LazyCompute compute={computeA} />);
    
  346.       expect(clearLog()).toEqual(['compute A', 'A']);
    
  347.       expect(domNode.tagName).toEqual('SPAN');
    
  348.       expect(domNode.textContent).toEqual('A');
    
  349.     });
    
  350. 
    
  351.     itRenders(
    
  352.       'multiple times when updates happen during the render phase',
    
  353.       async render => {
    
  354.         function CapitalizedText(props) {
    
  355.           const [text, setText] = useState(props.text);
    
  356.           const capitalizedText = useMemo(() => {
    
  357.             yieldValue(`Capitalize '${text}'`);
    
  358.             return text.toUpperCase();
    
  359.           }, [text]);
    
  360. 
    
  361.           if (text === 'hello') {
    
  362.             setText('hello, world.');
    
  363.           }
    
  364.           return <Text text={capitalizedText} />;
    
  365.         }
    
  366. 
    
  367.         const domNode = await render(<CapitalizedText text="hello" />);
    
  368.         expect(clearLog()).toEqual([
    
  369.           "Capitalize 'hello'",
    
  370.           "Capitalize 'hello, world.'",
    
  371.           'HELLO, WORLD.',
    
  372.         ]);
    
  373.         expect(domNode.tagName).toEqual('SPAN');
    
  374.         expect(domNode.textContent).toEqual('HELLO, WORLD.');
    
  375.       },
    
  376.     );
    
  377. 
    
  378.     itRenders(
    
  379.       'should only invoke the memoized function when the inputs change',
    
  380.       async render => {
    
  381.         function CapitalizedText(props) {
    
  382.           const [text, setText] = useState(props.text);
    
  383.           const [count, setCount] = useState(0);
    
  384.           const capitalizedText = useMemo(() => {
    
  385.             yieldValue(`Capitalize '${text}'`);
    
  386.             return text.toUpperCase();
    
  387.           }, [text]);
    
  388. 
    
  389.           yieldValue(count);
    
  390. 
    
  391.           if (count < 3) {
    
  392.             setCount(count + 1);
    
  393.           }
    
  394. 
    
  395.           if (text === 'hello' && count === 2) {
    
  396.             setText('hello, world.');
    
  397.           }
    
  398.           return <Text text={capitalizedText} />;
    
  399.         }
    
  400. 
    
  401.         const domNode = await render(<CapitalizedText text="hello" />);
    
  402.         expect(clearLog()).toEqual([
    
  403.           "Capitalize 'hello'",
    
  404.           0,
    
  405.           1,
    
  406.           2,
    
  407.           // `capitalizedText` only recomputes when the text has changed
    
  408.           "Capitalize 'hello, world.'",
    
  409.           3,
    
  410.           'HELLO, WORLD.',
    
  411.         ]);
    
  412.         expect(domNode.tagName).toEqual('SPAN');
    
  413.         expect(domNode.textContent).toEqual('HELLO, WORLD.');
    
  414.       },
    
  415.     );
    
  416. 
    
  417.     itRenders('with a warning for useState inside useMemo', async render => {
    
  418.       function App() {
    
  419.         useMemo(() => {
    
  420.           useState();
    
  421.           return 0;
    
  422.         });
    
  423.         return 'hi';
    
  424.       }
    
  425. 
    
  426.       const domNode = await render(<App />, 1);
    
  427.       expect(domNode.textContent).toEqual('hi');
    
  428.     });
    
  429. 
    
  430.     itRenders('with a warning for useRef inside useState', async render => {
    
  431.       function App() {
    
  432.         const [value] = useState(() => {
    
  433.           useRef(0);
    
  434.           return 0;
    
  435.         });
    
  436.         return value;
    
  437.       }
    
  438. 
    
  439.       const domNode = await render(<App />, 1);
    
  440.       expect(domNode.textContent).toEqual('0');
    
  441.     });
    
  442.   });
    
  443. 
    
  444.   describe('useRef', () => {
    
  445.     itRenders('basic render', async render => {
    
  446.       function Counter(props) {
    
  447.         const ref = useRef();
    
  448.         return <span ref={ref}>Hi</span>;
    
  449.       }
    
  450. 
    
  451.       const domNode = await render(<Counter />);
    
  452.       expect(domNode.textContent).toEqual('Hi');
    
  453.     });
    
  454. 
    
  455.     itRenders(
    
  456.       'multiple times when updates happen during the render phase',
    
  457.       async render => {
    
  458.         function Counter(props) {
    
  459.           const [count, setCount] = useState(0);
    
  460.           const ref = useRef();
    
  461. 
    
  462.           if (count < 3) {
    
  463.             const newCount = count + 1;
    
  464.             setCount(newCount);
    
  465.           }
    
  466. 
    
  467.           yieldValue(count);
    
  468. 
    
  469.           return <span ref={ref}>Count: {count}</span>;
    
  470.         }
    
  471. 
    
  472.         const domNode = await render(<Counter />);
    
  473.         expect(clearLog()).toEqual([0, 1, 2, 3]);
    
  474.         expect(domNode.textContent).toEqual('Count: 3');
    
  475.       },
    
  476.     );
    
  477. 
    
  478.     itRenders(
    
  479.       'always return the same reference through multiple renders',
    
  480.       async render => {
    
  481.         let firstRef = null;
    
  482.         function Counter(props) {
    
  483.           const [count, setCount] = useState(0);
    
  484.           const ref = useRef();
    
  485.           if (firstRef === null) {
    
  486.             firstRef = ref;
    
  487.           } else if (firstRef !== ref) {
    
  488.             throw new Error('should never change');
    
  489.           }
    
  490. 
    
  491.           if (count < 3) {
    
  492.             setCount(count + 1);
    
  493.           } else {
    
  494.             firstRef = null;
    
  495.           }
    
  496. 
    
  497.           yieldValue(count);
    
  498. 
    
  499.           return <span ref={ref}>Count: {count}</span>;
    
  500.         }
    
  501. 
    
  502.         const domNode = await render(<Counter />);
    
  503.         expect(clearLog()).toEqual([0, 1, 2, 3]);
    
  504.         expect(domNode.textContent).toEqual('Count: 3');
    
  505.       },
    
  506.     );
    
  507.   });
    
  508. 
    
  509.   describe('useEffect', () => {
    
  510.     const yields = [];
    
  511.     itRenders('should ignore effects on the server', async render => {
    
  512.       function Counter(props) {
    
  513.         useEffect(() => {
    
  514.           yieldValue('invoked on client');
    
  515.         });
    
  516.         return <Text text={'Count: ' + props.count} />;
    
  517.       }
    
  518. 
    
  519.       const domNode = await render(<Counter count={0} />);
    
  520.       yields.push(clearLog());
    
  521.       expect(domNode.tagName).toEqual('SPAN');
    
  522.       expect(domNode.textContent).toEqual('Count: 0');
    
  523.     });
    
  524. 
    
  525.     it('verifies yields in order', () => {
    
  526.       expect(yields).toEqual([
    
  527.         ['Count: 0'], // server render
    
  528.         ['Count: 0'], // server stream
    
  529.         ['Count: 0', 'invoked on client'], // clean render
    
  530.         ['Count: 0', 'invoked on client'], // hydrated render
    
  531.         // nothing yielded for bad markup
    
  532.       ]);
    
  533.     });
    
  534.   });
    
  535. 
    
  536.   describe('useCallback', () => {
    
  537.     itRenders('should not invoke the passed callbacks', async render => {
    
  538.       function Counter(props) {
    
  539.         useCallback(() => {
    
  540.           yieldValue('should not be invoked');
    
  541.         });
    
  542.         return <Text text={'Count: ' + props.count} />;
    
  543.       }
    
  544.       const domNode = await render(<Counter count={0} />);
    
  545.       expect(clearLog()).toEqual(['Count: 0']);
    
  546.       expect(domNode.tagName).toEqual('SPAN');
    
  547.       expect(domNode.textContent).toEqual('Count: 0');
    
  548.     });
    
  549. 
    
  550.     itRenders('should support render time callbacks', async render => {
    
  551.       function Counter(props) {
    
  552.         const renderCount = useCallback(increment => {
    
  553.           return 'Count: ' + (props.count + increment);
    
  554.         });
    
  555.         return <Text text={renderCount(3)} />;
    
  556.       }
    
  557.       const domNode = await render(<Counter count={2} />);
    
  558.       expect(clearLog()).toEqual(['Count: 5']);
    
  559.       expect(domNode.tagName).toEqual('SPAN');
    
  560.       expect(domNode.textContent).toEqual('Count: 5');
    
  561.     });
    
  562. 
    
  563.     itRenders(
    
  564.       'should only change the returned reference when the inputs change',
    
  565.       async render => {
    
  566.         function CapitalizedText(props) {
    
  567.           const [text, setText] = useState(props.text);
    
  568.           const [count, setCount] = useState(0);
    
  569.           const capitalizeText = useCallback(() => text.toUpperCase(), [text]);
    
  570.           yieldValue(capitalizeText);
    
  571.           if (count < 3) {
    
  572.             setCount(count + 1);
    
  573.           }
    
  574.           if (text === 'hello' && count === 2) {
    
  575.             setText('hello, world.');
    
  576.           }
    
  577.           return <Text text={capitalizeText()} />;
    
  578.         }
    
  579. 
    
  580.         const domNode = await render(<CapitalizedText text="hello" />);
    
  581.         const [first, second, third, fourth, result] = clearLog();
    
  582.         expect(first).toBe(second);
    
  583.         expect(second).toBe(third);
    
  584.         expect(third).not.toBe(fourth);
    
  585.         expect(result).toEqual('HELLO, WORLD.');
    
  586.         expect(domNode.tagName).toEqual('SPAN');
    
  587.         expect(domNode.textContent).toEqual('HELLO, WORLD.');
    
  588.       },
    
  589.     );
    
  590.   });
    
  591. 
    
  592.   describe('useImperativeHandle', () => {
    
  593.     it('should not be invoked on the server', async () => {
    
  594.       function Counter(props, ref) {
    
  595.         useImperativeHandle(ref, () => {
    
  596.           throw new Error('should not be invoked');
    
  597.         });
    
  598.         return <Text text={props.label + ': ' + ref.current} />;
    
  599.       }
    
  600.       Counter = forwardRef(Counter);
    
  601.       const counter = React.createRef();
    
  602.       counter.current = 0;
    
  603.       const domNode = await serverRender(
    
  604.         <Counter label="Count" ref={counter} />,
    
  605.       );
    
  606.       expect(clearLog()).toEqual(['Count: 0']);
    
  607.       expect(domNode.tagName).toEqual('SPAN');
    
  608.       expect(domNode.textContent).toEqual('Count: 0');
    
  609.     });
    
  610.   });
    
  611.   describe('useInsertionEffect', () => {
    
  612.     it('should warn when invoked during render', async () => {
    
  613.       function Counter() {
    
  614.         useInsertionEffect(() => {
    
  615.           throw new Error('should not be invoked');
    
  616.         });
    
  617. 
    
  618.         return <Text text="Count: 0" />;
    
  619.       }
    
  620.       const domNode = await serverRender(<Counter />, 1);
    
  621.       expect(clearLog()).toEqual(['Count: 0']);
    
  622.       expect(domNode.tagName).toEqual('SPAN');
    
  623.       expect(domNode.textContent).toEqual('Count: 0');
    
  624.     });
    
  625.   });
    
  626. 
    
  627.   describe('useLayoutEffect', () => {
    
  628.     it('should warn when invoked during render', async () => {
    
  629.       function Counter() {
    
  630.         useLayoutEffect(() => {
    
  631.           throw new Error('should not be invoked');
    
  632.         });
    
  633. 
    
  634.         return <Text text="Count: 0" />;
    
  635.       }
    
  636.       const domNode = await serverRender(<Counter />, 1);
    
  637.       expect(clearLog()).toEqual(['Count: 0']);
    
  638.       expect(domNode.tagName).toEqual('SPAN');
    
  639.       expect(domNode.textContent).toEqual('Count: 0');
    
  640.     });
    
  641.   });
    
  642. 
    
  643.   describe('useContext', () => {
    
  644.     itThrowsWhenRendering(
    
  645.       'if used inside a class component',
    
  646.       async render => {
    
  647.         const Context = React.createContext({}, () => {});
    
  648.         class Counter extends React.Component {
    
  649.           render() {
    
  650.             const [count] = useContext(Context);
    
  651.             return <Text text={count} />;
    
  652.           }
    
  653.         }
    
  654. 
    
  655.         return render(<Counter />);
    
  656.       },
    
  657.       'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    
  658.         ' one of the following reasons:\n' +
    
  659.         '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    
  660.         '2. You might be breaking the Rules of Hooks\n' +
    
  661.         '3. You might have more than one copy of React in the same app\n' +
    
  662.         'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
    
  663.     );
    
  664.   });
    
  665. 
    
  666.   describe('invalid hooks', () => {
    
  667.     it('warns when calling useRef inside useReducer', async () => {
    
  668.       function App() {
    
  669.         const [value, dispatch] = useReducer((state, action) => {
    
  670.           useRef(0);
    
  671.           return state + 1;
    
  672.         }, 0);
    
  673.         if (value === 0) {
    
  674.           dispatch();
    
  675.         }
    
  676.         return value;
    
  677.       }
    
  678. 
    
  679.       let error;
    
  680.       try {
    
  681.         await serverRender(<App />);
    
  682.       } catch (x) {
    
  683.         error = x;
    
  684.       }
    
  685.       expect(error).not.toBe(undefined);
    
  686.       expect(error.message).toContain(
    
  687.         'Rendered more hooks than during the previous render',
    
  688.       );
    
  689.     });
    
  690.   });
    
  691. 
    
  692.   itRenders(
    
  693.     'can use the same context multiple times in the same function',
    
  694.     async render => {
    
  695.       const Context = React.createContext({foo: 0, bar: 0, baz: 0});
    
  696. 
    
  697.       function Provider(props) {
    
  698.         return (
    
  699.           <Context.Provider
    
  700.             value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
    
  701.             {props.children}
    
  702.           </Context.Provider>
    
  703.         );
    
  704.       }
    
  705. 
    
  706.       function FooAndBar() {
    
  707.         const {foo} = useContext(Context);
    
  708.         const {bar} = useContext(Context);
    
  709.         return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
    
  710.       }
    
  711. 
    
  712.       function Baz() {
    
  713.         const {baz} = useContext(Context);
    
  714.         return <Text text={'Baz: ' + baz} />;
    
  715.       }
    
  716. 
    
  717.       class Indirection extends React.Component {
    
  718.         render() {
    
  719.           return this.props.children;
    
  720.         }
    
  721.       }
    
  722. 
    
  723.       function App(props) {
    
  724.         return (
    
  725.           <div>
    
  726.             <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
    
  727.               <Indirection>
    
  728.                 <Indirection>
    
  729.                   <FooAndBar />
    
  730.                 </Indirection>
    
  731.                 <Indirection>
    
  732.                   <Baz />
    
  733.                 </Indirection>
    
  734.               </Indirection>
    
  735.             </Provider>
    
  736.           </div>
    
  737.         );
    
  738.       }
    
  739. 
    
  740.       const domNode = await render(<App foo={1} bar={3} baz={5} />);
    
  741.       expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
    
  742.       expect(domNode.childNodes.length).toBe(2);
    
  743.       expect(domNode.firstChild.tagName).toEqual('SPAN');
    
  744.       expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
    
  745.       expect(domNode.lastChild.tagName).toEqual('SPAN');
    
  746.       expect(domNode.lastChild.textContent).toEqual('Baz: 5');
    
  747.     },
    
  748.   );
    
  749. 
    
  750.   describe('useDebugValue', () => {
    
  751.     itRenders('is a noop', async render => {
    
  752.       function Counter(props) {
    
  753.         const debugValue = useDebugValue(123);
    
  754.         return <Text text={typeof debugValue} />;
    
  755.       }
    
  756. 
    
  757.       const domNode = await render(<Counter />);
    
  758.       expect(domNode.textContent).toEqual('undefined');
    
  759.     });
    
  760.   });
    
  761. 
    
  762.   describe('readContext', () => {
    
  763.     function readContext(Context) {
    
  764.       const dispatcher =
    
  765.         React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
    
  766.           .ReactCurrentDispatcher.current;
    
  767.       return dispatcher.readContext(Context);
    
  768.     }
    
  769. 
    
  770.     itRenders(
    
  771.       'can read the same context multiple times in the same function',
    
  772.       async render => {
    
  773.         const Context = React.createContext(
    
  774.           {foo: 0, bar: 0, baz: 0},
    
  775.           (a, b) => {
    
  776.             let result = 0;
    
  777.             if (a.foo !== b.foo) {
    
  778.               result |= 0b001;
    
  779.             }
    
  780.             if (a.bar !== b.bar) {
    
  781.               result |= 0b010;
    
  782.             }
    
  783.             if (a.baz !== b.baz) {
    
  784.               result |= 0b100;
    
  785.             }
    
  786.             return result;
    
  787.           },
    
  788.         );
    
  789. 
    
  790.         function Provider(props) {
    
  791.           return (
    
  792.             <Context.Provider
    
  793.               value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
    
  794.               {props.children}
    
  795.             </Context.Provider>
    
  796.           );
    
  797.         }
    
  798. 
    
  799.         function FooAndBar() {
    
  800.           const {foo} = readContext(Context, 0b001);
    
  801.           const {bar} = readContext(Context, 0b010);
    
  802.           return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
    
  803.         }
    
  804. 
    
  805.         function Baz() {
    
  806.           const {baz} = readContext(Context, 0b100);
    
  807.           return <Text text={'Baz: ' + baz} />;
    
  808.         }
    
  809. 
    
  810.         class Indirection extends React.Component {
    
  811.           shouldComponentUpdate() {
    
  812.             return false;
    
  813.           }
    
  814.           render() {
    
  815.             return this.props.children;
    
  816.           }
    
  817.         }
    
  818. 
    
  819.         function App(props) {
    
  820.           return (
    
  821.             <div>
    
  822.               <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
    
  823.                 <Indirection>
    
  824.                   <Indirection>
    
  825.                     <FooAndBar />
    
  826.                   </Indirection>
    
  827.                   <Indirection>
    
  828.                     <Baz />
    
  829.                   </Indirection>
    
  830.                 </Indirection>
    
  831.               </Provider>
    
  832.             </div>
    
  833.           );
    
  834.         }
    
  835. 
    
  836.         const domNode = await render(<App foo={1} bar={3} baz={5} />);
    
  837.         expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
    
  838.         expect(domNode.childNodes.length).toBe(2);
    
  839.         expect(domNode.firstChild.tagName).toEqual('SPAN');
    
  840.         expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
    
  841.         expect(domNode.lastChild.tagName).toEqual('SPAN');
    
  842.         expect(domNode.lastChild.textContent).toEqual('Baz: 5');
    
  843.       },
    
  844.     );
    
  845. 
    
  846.     itRenders('with a warning inside useMemo and useReducer', async render => {
    
  847.       const Context = React.createContext(42);
    
  848. 
    
  849.       function ReadInMemo(props) {
    
  850.         const count = React.useMemo(() => readContext(Context), []);
    
  851.         return <Text text={count} />;
    
  852.       }
    
  853. 
    
  854.       function ReadInReducer(props) {
    
  855.         const [count, dispatch] = React.useReducer(() => readContext(Context));
    
  856.         if (count !== 42) {
    
  857.           dispatch();
    
  858.         }
    
  859.         return <Text text={count} />;
    
  860.       }
    
  861. 
    
  862.       const domNode1 = await render(<ReadInMemo />, 1);
    
  863.       expect(domNode1.textContent).toEqual('42');
    
  864. 
    
  865.       const domNode2 = await render(<ReadInReducer />, 1);
    
  866.       expect(domNode2.textContent).toEqual('42');
    
  867.     });
    
  868.   });
    
  869. 
    
  870.   it('renders successfully after a component using hooks throws an error', () => {
    
  871.     function ThrowingComponent() {
    
  872.       const [value, dispatch] = useReducer((state, action) => {
    
  873.         return state + 1;
    
  874.       }, 0);
    
  875. 
    
  876.       // throw an error if the count gets too high during the re-render phase
    
  877.       if (value >= 3) {
    
  878.         throw new Error('Error from ThrowingComponent');
    
  879.       } else {
    
  880.         // dispatch to trigger a re-render of the component
    
  881.         dispatch();
    
  882.       }
    
  883. 
    
  884.       return <div>{value}</div>;
    
  885.     }
    
  886. 
    
  887.     function NonThrowingComponent() {
    
  888.       const [count] = useState(0);
    
  889.       return <div>{count}</div>;
    
  890.     }
    
  891. 
    
  892.     // First, render a component that will throw an error during a re-render triggered
    
  893.     // by a dispatch call.
    
  894.     expect(() => ReactDOMServer.renderToString(<ThrowingComponent />)).toThrow(
    
  895.       'Error from ThrowingComponent',
    
  896.     );
    
  897. 
    
  898.     // Next, assert that we can render a function component using hooks immediately
    
  899.     // after an error occurred, which indictates the internal hooks state has been
    
  900.     // reset.
    
  901.     const container = document.createElement('div');
    
  902.     container.innerHTML = ReactDOMServer.renderToString(
    
  903.       <NonThrowingComponent />,
    
  904.     );
    
  905.     expect(container.children[0].textContent).toEqual('0');
    
  906.   });
    
  907. });