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.  * @flow
    
  8.  */
    
  9. 
    
  10. import typeof ReactTestRenderer from 'react-test-renderer';
    
  11. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  12. import type {Context} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext';
    
  13. import type {DispatcherContext} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';
    
  14. import type Store from 'react-devtools-shared/src/devtools/store';
    
  15. 
    
  16. describe('ProfilerContext', () => {
    
  17.   let React;
    
  18.   let ReactDOM;
    
  19.   let TestRenderer: ReactTestRenderer;
    
  20.   let bridge: FrontendBridge;
    
  21.   let legacyRender;
    
  22.   let store: Store;
    
  23.   let utils;
    
  24. 
    
  25.   let BridgeContext;
    
  26.   let ProfilerContext;
    
  27.   let ProfilerContextController;
    
  28.   let StoreContext;
    
  29.   let TreeContextController;
    
  30.   let TreeDispatcherContext;
    
  31.   let TreeStateContext;
    
  32. 
    
  33.   beforeEach(() => {
    
  34.     utils = require('./utils');
    
  35.     utils.beforeEachProfiling();
    
  36. 
    
  37.     legacyRender = utils.legacyRender;
    
  38. 
    
  39.     bridge = global.bridge;
    
  40.     store = global.store;
    
  41.     store.collapseNodesByDefault = false;
    
  42.     store.recordChangeDescriptions = true;
    
  43. 
    
  44.     React = require('react');
    
  45.     ReactDOM = require('react-dom');
    
  46.     TestRenderer = utils.requireTestRenderer();
    
  47. 
    
  48.     BridgeContext =
    
  49.       require('react-devtools-shared/src/devtools/views/context').BridgeContext;
    
  50.     ProfilerContext =
    
  51.       require('react-devtools-shared/src/devtools/views/Profiler/ProfilerContext').ProfilerContext;
    
  52.     ProfilerContextController =
    
  53.       require('react-devtools-shared/src/devtools/views/Profiler/ProfilerContext').ProfilerContextController;
    
  54.     StoreContext =
    
  55.       require('react-devtools-shared/src/devtools/views/context').StoreContext;
    
  56.     TreeContextController =
    
  57.       require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;
    
  58.     TreeDispatcherContext =
    
  59.       require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeDispatcherContext;
    
  60.     TreeStateContext =
    
  61.       require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeStateContext;
    
  62.   });
    
  63. 
    
  64.   const Contexts = ({
    
  65.     children = null,
    
  66.     defaultSelectedElementID = null,
    
  67.     defaultSelectedElementIndex = null,
    
  68.   }: any) => (
    
  69.     <BridgeContext.Provider value={bridge}>
    
  70.       <StoreContext.Provider value={store}>
    
  71.         <TreeContextController
    
  72.           defaultSelectedElementID={defaultSelectedElementID}
    
  73.           defaultSelectedElementIndex={defaultSelectedElementIndex}>
    
  74.           <ProfilerContextController>{children}</ProfilerContextController>
    
  75.         </TreeContextController>
    
  76.       </StoreContext.Provider>
    
  77.     </BridgeContext.Provider>
    
  78.   );
    
  79. 
    
  80.   it('updates updates profiling support based on the attached roots', async () => {
    
  81.     const Component = () => null;
    
  82. 
    
  83.     let context: Context = ((null: any): Context);
    
  84. 
    
  85.     function ContextReader() {
    
  86.       context = React.useContext(ProfilerContext);
    
  87.       return null;
    
  88.     }
    
  89.     await utils.actAsync(() => {
    
  90.       TestRenderer.create(
    
  91.         <Contexts>
    
  92.           <ContextReader />
    
  93.         </Contexts>,
    
  94.       );
    
  95.     });
    
  96. 
    
  97.     expect(context.supportsProfiling).toBe(false);
    
  98. 
    
  99.     const containerA = document.createElement('div');
    
  100.     const containerB = document.createElement('div');
    
  101. 
    
  102.     await utils.actAsync(() => legacyRender(<Component />, containerA));
    
  103.     expect(context.supportsProfiling).toBe(true);
    
  104. 
    
  105.     await utils.actAsync(() => legacyRender(<Component />, containerB));
    
  106.     await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerA));
    
  107.     expect(context.supportsProfiling).toBe(true);
    
  108. 
    
  109.     await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerB));
    
  110.     expect(context.supportsProfiling).toBe(false);
    
  111.   });
    
  112. 
    
  113.   it('should gracefully handle an empty profiling session (with no recorded commits)', async () => {
    
  114.     const Example = () => null;
    
  115. 
    
  116.     utils.act(() => legacyRender(<Example />, document.createElement('div')));
    
  117. 
    
  118.     let context: Context = ((null: any): Context);
    
  119. 
    
  120.     function ContextReader() {
    
  121.       context = React.useContext(ProfilerContext);
    
  122.       return null;
    
  123.     }
    
  124. 
    
  125.     // Profile but don't record any updates.
    
  126.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  127.     await utils.actAsync(() => {
    
  128.       TestRenderer.create(
    
  129.         <Contexts>
    
  130.           <ContextReader />
    
  131.         </Contexts>,
    
  132.       );
    
  133.     });
    
  134.     expect(context).not.toBeNull();
    
  135.     expect(context.didRecordCommits).toBe(false);
    
  136.     expect(context.isProcessingData).toBe(false);
    
  137.     expect(context.isProfiling).toBe(true);
    
  138.     expect(context.profilingData).toBe(null);
    
  139.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  140. 
    
  141.     expect(context).not.toBeNull();
    
  142.     expect(context.didRecordCommits).toBe(false);
    
  143.     expect(context.isProcessingData).toBe(false);
    
  144.     expect(context.isProfiling).toBe(false);
    
  145.     expect(context.profilingData).toBe(null);
    
  146.   });
    
  147. 
    
  148.   it('should auto-select the root ID matching the Components tab selection if it has profiling data', async () => {
    
  149.     const Parent = () => <Child />;
    
  150.     const Child = () => null;
    
  151. 
    
  152.     const containerOne = document.createElement('div');
    
  153.     const containerTwo = document.createElement('div');
    
  154.     utils.act(() => legacyRender(<Parent />, containerOne));
    
  155.     utils.act(() => legacyRender(<Parent />, containerTwo));
    
  156.     expect(store).toMatchInlineSnapshot(`
    
  157.       [root]
    
  158.         ▾ <Parent>
    
  159.             <Child>
    
  160.       [root]
    
  161.         ▾ <Parent>
    
  162.             <Child>
    
  163.     `);
    
  164. 
    
  165.     // Profile and record updates to both roots.
    
  166.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  167.     await utils.actAsync(() => legacyRender(<Parent />, containerOne));
    
  168.     await utils.actAsync(() => legacyRender(<Parent />, containerTwo));
    
  169.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  170. 
    
  171.     let context: Context = ((null: any): Context);
    
  172.     function ContextReader() {
    
  173.       context = React.useContext(ProfilerContext);
    
  174.       return null;
    
  175.     }
    
  176. 
    
  177.     // Select an element within the second root.
    
  178.     await utils.actAsync(() =>
    
  179.       TestRenderer.create(
    
  180.         <Contexts
    
  181.           defaultSelectedElementID={store.getElementIDAtIndex(3)}
    
  182.           defaultSelectedElementIndex={3}>
    
  183.           <ContextReader />
    
  184.         </Contexts>,
    
  185.       ),
    
  186.     );
    
  187. 
    
  188.     expect(context).not.toBeNull();
    
  189.     expect(context.rootID).toBe(
    
  190.       store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number)),
    
  191.     );
    
  192.   });
    
  193. 
    
  194.   it('should not select the root ID matching the Components tab selection if it has no profiling data', async () => {
    
  195.     const Parent = () => <Child />;
    
  196.     const Child = () => null;
    
  197. 
    
  198.     const containerOne = document.createElement('div');
    
  199.     const containerTwo = document.createElement('div');
    
  200.     utils.act(() => legacyRender(<Parent />, containerOne));
    
  201.     utils.act(() => legacyRender(<Parent />, containerTwo));
    
  202.     expect(store).toMatchInlineSnapshot(`
    
  203.       [root]
    
  204.         ▾ <Parent>
    
  205.             <Child>
    
  206.       [root]
    
  207.         ▾ <Parent>
    
  208.             <Child>
    
  209.     `);
    
  210. 
    
  211.     // Profile and record updates to only the first root.
    
  212.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  213.     await utils.actAsync(() => legacyRender(<Parent />, containerOne));
    
  214.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  215. 
    
  216.     let context: Context = ((null: any): Context);
    
  217.     function ContextReader() {
    
  218.       context = React.useContext(ProfilerContext);
    
  219.       return null;
    
  220.     }
    
  221. 
    
  222.     // Select an element within the second root.
    
  223.     await utils.actAsync(() =>
    
  224.       TestRenderer.create(
    
  225.         <Contexts
    
  226.           defaultSelectedElementID={store.getElementIDAtIndex(3)}
    
  227.           defaultSelectedElementIndex={3}>
    
  228.           <ContextReader />
    
  229.         </Contexts>,
    
  230.       ),
    
  231.     );
    
  232. 
    
  233.     // Verify the default profiling root is the first one.
    
  234.     expect(context).not.toBeNull();
    
  235.     expect(context.rootID).toBe(
    
  236.       store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number)),
    
  237.     );
    
  238.   });
    
  239. 
    
  240.   it('should maintain root selection between profiling sessions so long as there is data for that root', async () => {
    
  241.     const Parent = () => <Child />;
    
  242.     const Child = () => null;
    
  243. 
    
  244.     const containerA = document.createElement('div');
    
  245.     const containerB = document.createElement('div');
    
  246.     utils.act(() => legacyRender(<Parent />, containerA));
    
  247.     utils.act(() => legacyRender(<Parent />, containerB));
    
  248.     expect(store).toMatchInlineSnapshot(`
    
  249.       [root]
    
  250.         ▾ <Parent>
    
  251.             <Child>
    
  252.       [root]
    
  253.         ▾ <Parent>
    
  254.             <Child>
    
  255.     `);
    
  256. 
    
  257.     // Profile and record updates.
    
  258.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  259.     await utils.actAsync(() => legacyRender(<Parent />, containerA));
    
  260.     await utils.actAsync(() => legacyRender(<Parent />, containerB));
    
  261.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  262. 
    
  263.     let context: Context = ((null: any): Context);
    
  264.     let dispatch: DispatcherContext = ((null: any): DispatcherContext);
    
  265.     let selectedElementID = null;
    
  266.     function ContextReader() {
    
  267.       context = React.useContext(ProfilerContext);
    
  268.       dispatch = React.useContext(TreeDispatcherContext);
    
  269.       selectedElementID = React.useContext(TreeStateContext).selectedElementID;
    
  270.       return null;
    
  271.     }
    
  272. 
    
  273.     const id = ((store.getElementIDAtIndex(3): any): number);
    
  274. 
    
  275.     // Select an element within the second root.
    
  276.     await utils.actAsync(() =>
    
  277.       TestRenderer.create(
    
  278.         <Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
    
  279.           <ContextReader />
    
  280.         </Contexts>,
    
  281.       ),
    
  282.     );
    
  283. 
    
  284.     expect(selectedElementID).toBe(id);
    
  285. 
    
  286.     // Profile and record more updates to both roots
    
  287.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  288.     await utils.actAsync(() => legacyRender(<Parent />, containerA));
    
  289.     await utils.actAsync(() => legacyRender(<Parent />, containerB));
    
  290.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  291. 
    
  292.     const otherID = ((store.getElementIDAtIndex(0): any): number);
    
  293. 
    
  294.     // Change the selected element within a the Components tab.
    
  295.     utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
    
  296. 
    
  297.     // Verify that the initial Profiler root selection is maintained.
    
  298.     expect(selectedElementID).toBe(otherID);
    
  299.     expect(context).not.toBeNull();
    
  300.     expect(context.rootID).toBe(store.getRootIDForElement(id));
    
  301.   });
    
  302. 
    
  303.   it('should sync selected element in the Components tab too, provided the element is a match', async () => {
    
  304.     const GrandParent = ({includeChild}) => (
    
  305.       <Parent includeChild={includeChild} />
    
  306.     );
    
  307.     const Parent = ({includeChild}) => (includeChild ? <Child /> : null);
    
  308.     const Child = () => null;
    
  309. 
    
  310.     const container = document.createElement('div');
    
  311.     utils.act(() =>
    
  312.       legacyRender(<GrandParent includeChild={true} />, container),
    
  313.     );
    
  314.     expect(store).toMatchInlineSnapshot(`
    
  315.       [root]
    
  316.         ▾ <GrandParent>
    
  317.           ▾ <Parent>
    
  318.               <Child>
    
  319.     `);
    
  320. 
    
  321.     const parentID = ((store.getElementIDAtIndex(1): any): number);
    
  322.     const childID = ((store.getElementIDAtIndex(2): any): number);
    
  323. 
    
  324.     // Profile and record updates.
    
  325.     await utils.actAsync(() => store.profilerStore.startProfiling());
    
  326.     await utils.actAsync(() =>
    
  327.       legacyRender(<GrandParent includeChild={true} />, container),
    
  328.     );
    
  329.     await utils.actAsync(() =>
    
  330.       legacyRender(<GrandParent includeChild={false} />, container),
    
  331.     );
    
  332.     await utils.actAsync(() => store.profilerStore.stopProfiling());
    
  333. 
    
  334.     expect(store).toMatchInlineSnapshot(`
    
  335.       [root]
    
  336.         ▾ <GrandParent>
    
  337.             <Parent>
    
  338.     `);
    
  339. 
    
  340.     let context: Context = ((null: any): Context);
    
  341.     let selectedElementID = null;
    
  342.     function ContextReader() {
    
  343.       context = React.useContext(ProfilerContext);
    
  344.       selectedElementID = React.useContext(TreeStateContext).selectedElementID;
    
  345.       return null;
    
  346.     }
    
  347. 
    
  348.     await utils.actAsync(() =>
    
  349.       TestRenderer.create(
    
  350.         <Contexts>
    
  351.           <ContextReader />
    
  352.         </Contexts>,
    
  353.       ),
    
  354.     );
    
  355.     expect(selectedElementID).toBeNull();
    
  356. 
    
  357.     // Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
    
  358.     await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
    
  359.     expect(selectedElementID).toBe(parentID);
    
  360. 
    
  361.     // Select an unmounted element and verify no Components tab selection doesn't change.
    
  362.     await utils.actAsync(() => context.selectFiber(childID, 'Child'));
    
  363.     expect(selectedElementID).toBe(parentID);
    
  364.   });
    
  365. });