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 node
    
  9.  */
    
  10. 
    
  11. 'use strict';
    
  12. 
    
  13. describe('React hooks DevTools integration', () => {
    
  14.   let React;
    
  15.   let ReactDebugTools;
    
  16.   let ReactTestRenderer;
    
  17.   let act;
    
  18.   let overrideHookState;
    
  19.   let scheduleUpdate;
    
  20.   let setSuspenseHandler;
    
  21.   let waitForAll;
    
  22. 
    
  23.   global.IS_REACT_ACT_ENVIRONMENT = true;
    
  24. 
    
  25.   beforeEach(() => {
    
  26.     global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
    
  27.       inject: injected => {
    
  28.         overrideHookState = injected.overrideHookState;
    
  29.         scheduleUpdate = injected.scheduleUpdate;
    
  30.         setSuspenseHandler = injected.setSuspenseHandler;
    
  31.       },
    
  32.       supportsFiber: true,
    
  33.       onCommitFiberRoot: () => {},
    
  34.       onCommitFiberUnmount: () => {},
    
  35.     };
    
  36. 
    
  37.     jest.resetModules();
    
  38. 
    
  39.     React = require('react');
    
  40.     ReactDebugTools = require('react-debug-tools');
    
  41.     ReactTestRenderer = require('react-test-renderer');
    
  42. 
    
  43.     const InternalTestUtils = require('internal-test-utils');
    
  44.     waitForAll = InternalTestUtils.waitForAll;
    
  45. 
    
  46.     act = ReactTestRenderer.act;
    
  47.   });
    
  48. 
    
  49.   it('should support editing useState hooks', async () => {
    
  50.     let setCountFn;
    
  51. 
    
  52.     function MyComponent() {
    
  53.       const [count, setCount] = React.useState(0);
    
  54.       setCountFn = setCount;
    
  55.       return <div>count:{count}</div>;
    
  56.     }
    
  57. 
    
  58.     const renderer = ReactTestRenderer.create(<MyComponent />);
    
  59.     expect(renderer.toJSON()).toEqual({
    
  60.       type: 'div',
    
  61.       props: {},
    
  62.       children: ['count:', '0'],
    
  63.     });
    
  64. 
    
  65.     const fiber = renderer.root.findByType(MyComponent)._currentFiber();
    
  66.     const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
    
  67.     const stateHook = tree[0];
    
  68.     expect(stateHook.isStateEditable).toBe(true);
    
  69. 
    
  70.     if (__DEV__) {
    
  71.       await act(() => overrideHookState(fiber, stateHook.id, [], 10));
    
  72.       expect(renderer.toJSON()).toEqual({
    
  73.         type: 'div',
    
  74.         props: {},
    
  75.         children: ['count:', '10'],
    
  76.       });
    
  77. 
    
  78.       await act(() => setCountFn(count => count + 1));
    
  79.       expect(renderer.toJSON()).toEqual({
    
  80.         type: 'div',
    
  81.         props: {},
    
  82.         children: ['count:', '11'],
    
  83.       });
    
  84.     }
    
  85.   });
    
  86. 
    
  87.   it('should support editable useReducer hooks', async () => {
    
  88.     const initialData = {foo: 'abc', bar: 123};
    
  89. 
    
  90.     function reducer(state, action) {
    
  91.       switch (action.type) {
    
  92.         case 'swap':
    
  93.           return {foo: state.bar, bar: state.foo};
    
  94.         default:
    
  95.           throw new Error();
    
  96.       }
    
  97.     }
    
  98. 
    
  99.     let dispatchFn;
    
  100.     function MyComponent() {
    
  101.       const [state, dispatch] = React.useReducer(reducer, initialData);
    
  102.       dispatchFn = dispatch;
    
  103.       return (
    
  104.         <div>
    
  105.           foo:{state.foo}, bar:{state.bar}
    
  106.         </div>
    
  107.       );
    
  108.     }
    
  109. 
    
  110.     const renderer = ReactTestRenderer.create(<MyComponent />);
    
  111.     expect(renderer.toJSON()).toEqual({
    
  112.       type: 'div',
    
  113.       props: {},
    
  114.       children: ['foo:', 'abc', ', bar:', '123'],
    
  115.     });
    
  116. 
    
  117.     const fiber = renderer.root.findByType(MyComponent)._currentFiber();
    
  118.     const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
    
  119.     const reducerHook = tree[0];
    
  120.     expect(reducerHook.isStateEditable).toBe(true);
    
  121. 
    
  122.     if (__DEV__) {
    
  123.       await act(() => overrideHookState(fiber, reducerHook.id, ['foo'], 'def'));
    
  124.       expect(renderer.toJSON()).toEqual({
    
  125.         type: 'div',
    
  126.         props: {},
    
  127.         children: ['foo:', 'def', ', bar:', '123'],
    
  128.       });
    
  129. 
    
  130.       await act(() => dispatchFn({type: 'swap'}));
    
  131.       expect(renderer.toJSON()).toEqual({
    
  132.         type: 'div',
    
  133.         props: {},
    
  134.         children: ['foo:', '123', ', bar:', 'def'],
    
  135.       });
    
  136.     }
    
  137.   });
    
  138. 
    
  139.   // This test case is based on an open source bug report:
    
  140.   // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787
    
  141.   it('should handle interleaved stateful hooks (e.g. useState) and non-stateful hooks (e.g. useContext)', async () => {
    
  142.     const MyContext = React.createContext(1);
    
  143. 
    
  144.     let setStateFn;
    
  145.     function useCustomHook() {
    
  146.       const context = React.useContext(MyContext);
    
  147.       const [state, setState] = React.useState({count: context});
    
  148.       React.useDebugValue(state.count);
    
  149.       setStateFn = setState;
    
  150.       return state.count;
    
  151.     }
    
  152. 
    
  153.     function MyComponent() {
    
  154.       const count = useCustomHook();
    
  155.       return <div>count:{count}</div>;
    
  156.     }
    
  157. 
    
  158.     const renderer = ReactTestRenderer.create(<MyComponent />);
    
  159.     expect(renderer.toJSON()).toEqual({
    
  160.       type: 'div',
    
  161.       props: {},
    
  162.       children: ['count:', '1'],
    
  163.     });
    
  164. 
    
  165.     const fiber = renderer.root.findByType(MyComponent)._currentFiber();
    
  166.     const tree = ReactDebugTools.inspectHooksOfFiber(fiber);
    
  167.     const stateHook = tree[0].subHooks[1];
    
  168.     expect(stateHook.isStateEditable).toBe(true);
    
  169. 
    
  170.     if (__DEV__) {
    
  171.       await act(() => overrideHookState(fiber, stateHook.id, ['count'], 10));
    
  172.       expect(renderer.toJSON()).toEqual({
    
  173.         type: 'div',
    
  174.         props: {},
    
  175.         children: ['count:', '10'],
    
  176.       });
    
  177.       await act(() => setStateFn(state => ({count: state.count + 1})));
    
  178.       expect(renderer.toJSON()).toEqual({
    
  179.         type: 'div',
    
  180.         props: {},
    
  181.         children: ['count:', '11'],
    
  182.       });
    
  183.     }
    
  184.   });
    
  185. 
    
  186.   it('should support overriding suspense in legacy mode', async () => {
    
  187.     if (__DEV__) {
    
  188.       // Lock the first render
    
  189.       setSuspenseHandler(() => true);
    
  190.     }
    
  191. 
    
  192.     function MyComponent() {
    
  193.       return 'Done';
    
  194.     }
    
  195. 
    
  196.     const renderer = ReactTestRenderer.create(
    
  197.       <div>
    
  198.         <React.Suspense fallback={'Loading'}>
    
  199.           <MyComponent />
    
  200.         </React.Suspense>
    
  201.       </div>,
    
  202.     );
    
  203.     const fiber = renderer.root._currentFiber().child;
    
  204.     if (__DEV__) {
    
  205.       // First render was locked
    
  206.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  207.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  208.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  209. 
    
  210.       // Release the lock
    
  211.       setSuspenseHandler(() => false);
    
  212.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  213.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  214.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  215.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  216. 
    
  217.       // Lock again
    
  218.       setSuspenseHandler(() => true);
    
  219.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  220.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  221. 
    
  222.       // Release the lock again
    
  223.       setSuspenseHandler(() => false);
    
  224.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  225.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  226. 
    
  227.       // Ensure it checks specific fibers.
    
  228.       setSuspenseHandler(f => f === fiber || f === fiber.alternate);
    
  229.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  230.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  231.       setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
    
  232.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  233.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  234.     } else {
    
  235.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  236.     }
    
  237.   });
    
  238. 
    
  239.   // @gate __DEV__
    
  240.   it('should support overriding suspense in concurrent mode', async () => {
    
  241.     if (__DEV__) {
    
  242.       // Lock the first render
    
  243.       setSuspenseHandler(() => true);
    
  244.     }
    
  245. 
    
  246.     function MyComponent() {
    
  247.       return 'Done';
    
  248.     }
    
  249. 
    
  250.     const renderer = await act(() =>
    
  251.       ReactTestRenderer.create(
    
  252.         <div>
    
  253.           <React.Suspense fallback={'Loading'}>
    
  254.             <MyComponent />
    
  255.           </React.Suspense>
    
  256.         </div>,
    
  257.         {unstable_isConcurrent: true},
    
  258.       ),
    
  259.     );
    
  260. 
    
  261.     await waitForAll([]);
    
  262.     // Ensure we timeout any suspense time.
    
  263.     jest.advanceTimersByTime(1000);
    
  264.     const fiber = renderer.root._currentFiber().child;
    
  265.     if (__DEV__) {
    
  266.       // First render was locked
    
  267.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  268.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  269.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  270. 
    
  271.       // Release the lock
    
  272.       setSuspenseHandler(() => false);
    
  273.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  274.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  275.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  276.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  277. 
    
  278.       // Lock again
    
  279.       setSuspenseHandler(() => true);
    
  280.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  281.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  282. 
    
  283.       // Release the lock again
    
  284.       setSuspenseHandler(() => false);
    
  285.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  286.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  287. 
    
  288.       // Ensure it checks specific fibers.
    
  289.       setSuspenseHandler(f => f === fiber || f === fiber.alternate);
    
  290.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  291.       expect(renderer.toJSON().children).toEqual(['Loading']);
    
  292.       setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
    
  293.       await act(() => scheduleUpdate(fiber)); // Re-render
    
  294.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  295.     } else {
    
  296.       expect(renderer.toJSON().children).toEqual(['Done']);
    
  297.     }
    
  298.   });
    
  299. });