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 type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  11. import type Store from 'react-devtools-shared/src/devtools/store';
    
  12. 
    
  13. describe('editing interface', () => {
    
  14.   let PropTypes;
    
  15.   let React;
    
  16.   let ReactDOM;
    
  17.   let bridge: FrontendBridge;
    
  18.   let store: Store;
    
  19. 
    
  20.   const act = (callback: Function) => {
    
  21.     callback();
    
  22. 
    
  23.     jest.runAllTimers(); // Flush Bridge operations
    
  24.   };
    
  25. 
    
  26.   const flushPendingUpdates = () => {
    
  27.     jest.runOnlyPendingTimers();
    
  28.   };
    
  29. 
    
  30.   beforeEach(() => {
    
  31.     bridge = global.bridge;
    
  32.     store = global.store;
    
  33.     store.collapseNodesByDefault = false;
    
  34. 
    
  35.     PropTypes = require('prop-types');
    
  36. 
    
  37.     // Redirect all React/ReactDOM requires to the v15 UMD.
    
  38.     // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
    
  39.     jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
    
  40.     jest.mock('react-dom', () =>
    
  41.       jest.requireActual('react-dom-15/dist/react-dom.js'),
    
  42.     );
    
  43. 
    
  44.     React = require('react');
    
  45.     ReactDOM = require('react-dom');
    
  46.   });
    
  47. 
    
  48.   describe('props', () => {
    
  49.     let committedProps;
    
  50.     let id;
    
  51. 
    
  52.     function mountTestApp() {
    
  53.       class ClassComponent extends React.Component {
    
  54.         componentDidMount() {
    
  55.           committedProps = this.props;
    
  56.         }
    
  57.         componentDidUpdate() {
    
  58.           committedProps = this.props;
    
  59.         }
    
  60.         render() {
    
  61.           return null;
    
  62.         }
    
  63.       }
    
  64. 
    
  65.       act(() =>
    
  66.         ReactDOM.render(
    
  67.           <ClassComponent
    
  68.             array={[1, 2, 3]}
    
  69.             object={{nested: 'initial'}}
    
  70.             shallow="initial"
    
  71.           />,
    
  72.           document.createElement('div'),
    
  73.         ),
    
  74.       );
    
  75. 
    
  76.       id = ((store.getElementIDAtIndex(0): any): number);
    
  77. 
    
  78.       expect(committedProps).toStrictEqual({
    
  79.         array: [1, 2, 3],
    
  80.         object: {
    
  81.           nested: 'initial',
    
  82.         },
    
  83.         shallow: 'initial',
    
  84.       });
    
  85.     }
    
  86. 
    
  87.     // @reactVersion >= 16.0
    
  88.     it('should have editable values', () => {
    
  89.       mountTestApp();
    
  90. 
    
  91.       function overrideProps(path, value) {
    
  92.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  93.         bridge.send('overrideValueAtPath', {
    
  94.           id,
    
  95.           path,
    
  96.           rendererID,
    
  97.           type: 'props',
    
  98.           value,
    
  99.         });
    
  100.         flushPendingUpdates();
    
  101.       }
    
  102. 
    
  103.       overrideProps(['shallow'], 'updated');
    
  104.       expect(committedProps).toStrictEqual({
    
  105.         array: [1, 2, 3],
    
  106.         object: {
    
  107.           nested: 'initial',
    
  108.         },
    
  109.         shallow: 'updated',
    
  110.       });
    
  111.       overrideProps(['object', 'nested'], 'updated');
    
  112.       expect(committedProps).toStrictEqual({
    
  113.         array: [1, 2, 3],
    
  114.         object: {
    
  115.           nested: 'updated',
    
  116.         },
    
  117.         shallow: 'updated',
    
  118.       });
    
  119.       overrideProps(['array', 1], 'updated');
    
  120.       expect(committedProps).toStrictEqual({
    
  121.         array: [1, 'updated', 3],
    
  122.         object: {
    
  123.           nested: 'updated',
    
  124.         },
    
  125.         shallow: 'updated',
    
  126.       });
    
  127.     });
    
  128. 
    
  129.     // @reactVersion >= 16.0
    
  130.     it('should have editable paths', () => {
    
  131.       mountTestApp();
    
  132. 
    
  133.       function renamePath(oldPath, newPath) {
    
  134.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  135.         bridge.send('renamePath', {
    
  136.           id,
    
  137.           oldPath,
    
  138.           newPath,
    
  139.           rendererID,
    
  140.           type: 'props',
    
  141.         });
    
  142.         flushPendingUpdates();
    
  143.       }
    
  144. 
    
  145.       renamePath(['shallow'], ['after']);
    
  146.       expect(committedProps).toStrictEqual({
    
  147.         array: [1, 2, 3],
    
  148.         object: {
    
  149.           nested: 'initial',
    
  150.         },
    
  151.         after: 'initial',
    
  152.       });
    
  153.       renamePath(['object', 'nested'], ['object', 'after']);
    
  154.       expect(committedProps).toStrictEqual({
    
  155.         array: [1, 2, 3],
    
  156.         object: {
    
  157.           after: 'initial',
    
  158.         },
    
  159.         after: 'initial',
    
  160.       });
    
  161.     });
    
  162. 
    
  163.     // @reactVersion >= 16.0
    
  164.     it('should enable adding new object properties and array values', async () => {
    
  165.       await mountTestApp();
    
  166. 
    
  167.       function overrideProps(path, value) {
    
  168.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  169.         bridge.send('overrideValueAtPath', {
    
  170.           id,
    
  171.           path,
    
  172.           rendererID,
    
  173.           type: 'props',
    
  174.           value,
    
  175.         });
    
  176.         flushPendingUpdates();
    
  177.       }
    
  178. 
    
  179.       overrideProps(['new'], 'value');
    
  180.       expect(committedProps).toStrictEqual({
    
  181.         array: [1, 2, 3],
    
  182.         object: {
    
  183.           nested: 'initial',
    
  184.         },
    
  185.         shallow: 'initial',
    
  186.         new: 'value',
    
  187.       });
    
  188. 
    
  189.       overrideProps(['object', 'new'], 'value');
    
  190.       expect(committedProps).toStrictEqual({
    
  191.         array: [1, 2, 3],
    
  192.         object: {
    
  193.           nested: 'initial',
    
  194.           new: 'value',
    
  195.         },
    
  196.         shallow: 'initial',
    
  197.         new: 'value',
    
  198.       });
    
  199. 
    
  200.       overrideProps(['array', 3], 'new value');
    
  201.       expect(committedProps).toStrictEqual({
    
  202.         array: [1, 2, 3, 'new value'],
    
  203.         object: {
    
  204.           nested: 'initial',
    
  205.           new: 'value',
    
  206.         },
    
  207.         shallow: 'initial',
    
  208.         new: 'value',
    
  209.       });
    
  210.     });
    
  211. 
    
  212.     // @reactVersion >= 16.0
    
  213.     it('should have deletable keys', () => {
    
  214.       mountTestApp();
    
  215. 
    
  216.       function deletePath(path) {
    
  217.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  218.         bridge.send('deletePath', {
    
  219.           id,
    
  220.           path,
    
  221.           rendererID,
    
  222.           type: 'props',
    
  223.         });
    
  224.         flushPendingUpdates();
    
  225.       }
    
  226. 
    
  227.       deletePath(['shallow']);
    
  228.       expect(committedProps).toStrictEqual({
    
  229.         array: [1, 2, 3],
    
  230.         object: {
    
  231.           nested: 'initial',
    
  232.         },
    
  233.       });
    
  234.       deletePath(['object', 'nested']);
    
  235.       expect(committedProps).toStrictEqual({
    
  236.         array: [1, 2, 3],
    
  237.         object: {},
    
  238.       });
    
  239.       deletePath(['array', 1]);
    
  240.       expect(committedProps).toStrictEqual({
    
  241.         array: [1, 3],
    
  242.         object: {},
    
  243.       });
    
  244.     });
    
  245.   });
    
  246. 
    
  247.   describe('state', () => {
    
  248.     let committedState;
    
  249.     let id;
    
  250. 
    
  251.     function mountTestApp() {
    
  252.       class ClassComponent extends React.Component {
    
  253.         state = {
    
  254.           array: [1, 2, 3],
    
  255.           object: {
    
  256.             nested: 'initial',
    
  257.           },
    
  258.           shallow: 'initial',
    
  259.         };
    
  260.         componentDidMount() {
    
  261.           committedState = this.state;
    
  262.         }
    
  263.         componentDidUpdate() {
    
  264.           committedState = this.state;
    
  265.         }
    
  266.         render() {
    
  267.           return null;
    
  268.         }
    
  269.       }
    
  270. 
    
  271.       act(() =>
    
  272.         ReactDOM.render(
    
  273.           <ClassComponent object={{nested: 'initial'}} shallow="initial" />,
    
  274.           document.createElement('div'),
    
  275.         ),
    
  276.       );
    
  277. 
    
  278.       id = ((store.getElementIDAtIndex(0): any): number);
    
  279. 
    
  280.       expect(committedState).toStrictEqual({
    
  281.         array: [1, 2, 3],
    
  282.         object: {
    
  283.           nested: 'initial',
    
  284.         },
    
  285.         shallow: 'initial',
    
  286.       });
    
  287.     }
    
  288. 
    
  289.     // @reactVersion >= 16.0
    
  290.     it('should have editable values', () => {
    
  291.       mountTestApp();
    
  292. 
    
  293.       function overrideState(path, value) {
    
  294.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  295.         bridge.send('overrideValueAtPath', {
    
  296.           id,
    
  297.           path,
    
  298.           rendererID,
    
  299.           type: 'state',
    
  300.           value,
    
  301.         });
    
  302.         flushPendingUpdates();
    
  303.       }
    
  304. 
    
  305.       overrideState(['shallow'], 'updated');
    
  306.       expect(committedState).toStrictEqual({
    
  307.         array: [1, 2, 3],
    
  308.         object: {nested: 'initial'},
    
  309.         shallow: 'updated',
    
  310.       });
    
  311. 
    
  312.       overrideState(['object', 'nested'], 'updated');
    
  313.       expect(committedState).toStrictEqual({
    
  314.         array: [1, 2, 3],
    
  315.         object: {nested: 'updated'},
    
  316.         shallow: 'updated',
    
  317.       });
    
  318. 
    
  319.       overrideState(['array', 1], 'updated');
    
  320.       expect(committedState).toStrictEqual({
    
  321.         array: [1, 'updated', 3],
    
  322.         object: {nested: 'updated'},
    
  323.         shallow: 'updated',
    
  324.       });
    
  325.     });
    
  326. 
    
  327.     // @reactVersion >= 16.0
    
  328.     it('should have editable paths', () => {
    
  329.       mountTestApp();
    
  330. 
    
  331.       function renamePath(oldPath, newPath) {
    
  332.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  333.         bridge.send('renamePath', {
    
  334.           id,
    
  335.           oldPath,
    
  336.           newPath,
    
  337.           rendererID,
    
  338.           type: 'state',
    
  339.         });
    
  340.         flushPendingUpdates();
    
  341.       }
    
  342. 
    
  343.       renamePath(['shallow'], ['after']);
    
  344.       expect(committedState).toStrictEqual({
    
  345.         array: [1, 2, 3],
    
  346.         object: {
    
  347.           nested: 'initial',
    
  348.         },
    
  349.         after: 'initial',
    
  350.       });
    
  351. 
    
  352.       renamePath(['object', 'nested'], ['object', 'after']);
    
  353.       expect(committedState).toStrictEqual({
    
  354.         array: [1, 2, 3],
    
  355.         object: {
    
  356.           after: 'initial',
    
  357.         },
    
  358.         after: 'initial',
    
  359.       });
    
  360.     });
    
  361. 
    
  362.     // @reactVersion >= 16.0
    
  363.     it('should enable adding new object properties and array values', async () => {
    
  364.       await mountTestApp();
    
  365. 
    
  366.       function overrideState(path, value) {
    
  367.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  368.         bridge.send('overrideValueAtPath', {
    
  369.           id,
    
  370.           path,
    
  371.           rendererID,
    
  372.           type: 'state',
    
  373.           value,
    
  374.         });
    
  375.         flushPendingUpdates();
    
  376.       }
    
  377. 
    
  378.       overrideState(['new'], 'value');
    
  379.       expect(committedState).toStrictEqual({
    
  380.         array: [1, 2, 3],
    
  381.         object: {
    
  382.           nested: 'initial',
    
  383.         },
    
  384.         shallow: 'initial',
    
  385.         new: 'value',
    
  386.       });
    
  387. 
    
  388.       overrideState(['object', 'new'], 'value');
    
  389.       expect(committedState).toStrictEqual({
    
  390.         array: [1, 2, 3],
    
  391.         object: {
    
  392.           nested: 'initial',
    
  393.           new: 'value',
    
  394.         },
    
  395.         shallow: 'initial',
    
  396.         new: 'value',
    
  397.       });
    
  398. 
    
  399.       overrideState(['array', 3], 'new value');
    
  400.       expect(committedState).toStrictEqual({
    
  401.         array: [1, 2, 3, 'new value'],
    
  402.         object: {
    
  403.           nested: 'initial',
    
  404.           new: 'value',
    
  405.         },
    
  406.         shallow: 'initial',
    
  407.         new: 'value',
    
  408.       });
    
  409.     });
    
  410. 
    
  411.     // @reactVersion >= 16.0
    
  412.     it('should have deletable keys', () => {
    
  413.       mountTestApp();
    
  414. 
    
  415.       function deletePath(path) {
    
  416.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  417.         bridge.send('deletePath', {
    
  418.           id,
    
  419.           path,
    
  420.           rendererID,
    
  421.           type: 'state',
    
  422.         });
    
  423.         flushPendingUpdates();
    
  424.       }
    
  425. 
    
  426.       deletePath(['shallow']);
    
  427.       expect(committedState).toStrictEqual({
    
  428.         array: [1, 2, 3],
    
  429.         object: {
    
  430.           nested: 'initial',
    
  431.         },
    
  432.       });
    
  433. 
    
  434.       deletePath(['object', 'nested']);
    
  435.       expect(committedState).toStrictEqual({
    
  436.         array: [1, 2, 3],
    
  437.         object: {},
    
  438.       });
    
  439. 
    
  440.       deletePath(['array', 1]);
    
  441.       expect(committedState).toStrictEqual({
    
  442.         array: [1, 3],
    
  443.         object: {},
    
  444.       });
    
  445.     });
    
  446.   });
    
  447. 
    
  448.   describe('context', () => {
    
  449.     let committedContext;
    
  450.     let id;
    
  451. 
    
  452.     function mountTestApp() {
    
  453.       class LegacyContextProvider extends React.Component<any> {
    
  454.         static childContextTypes = {
    
  455.           array: PropTypes.array,
    
  456.           object: PropTypes.object,
    
  457.           shallow: PropTypes.string,
    
  458.         };
    
  459.         getChildContext() {
    
  460.           return {
    
  461.             array: [1, 2, 3],
    
  462.             object: {
    
  463.               nested: 'initial',
    
  464.             },
    
  465.             shallow: 'initial',
    
  466.           };
    
  467.         }
    
  468.         render() {
    
  469.           return this.props.children;
    
  470.         }
    
  471.       }
    
  472. 
    
  473.       class ClassComponent extends React.Component<any> {
    
  474.         static contextTypes = {
    
  475.           array: PropTypes.array,
    
  476.           object: PropTypes.object,
    
  477.           shallow: PropTypes.string,
    
  478.         };
    
  479.         componentDidMount() {
    
  480.           committedContext = this.context;
    
  481.         }
    
  482.         componentDidUpdate() {
    
  483.           committedContext = this.context;
    
  484.         }
    
  485.         render() {
    
  486.           return null;
    
  487.         }
    
  488.       }
    
  489. 
    
  490.       act(() =>
    
  491.         ReactDOM.render(
    
  492.           <LegacyContextProvider>
    
  493.             <ClassComponent />
    
  494.           </LegacyContextProvider>,
    
  495.           document.createElement('div'),
    
  496.         ),
    
  497.       );
    
  498. 
    
  499.       // This test only covers Class components.
    
  500.       // Function components using legacy context are not editable.
    
  501. 
    
  502.       id = ((store.getElementIDAtIndex(1): any): number);
    
  503. 
    
  504.       expect(committedContext).toStrictEqual({
    
  505.         array: [1, 2, 3],
    
  506.         object: {
    
  507.           nested: 'initial',
    
  508.         },
    
  509.         shallow: 'initial',
    
  510.       });
    
  511.     }
    
  512. 
    
  513.     // @reactVersion >= 16.0
    
  514.     it('should have editable values', () => {
    
  515.       mountTestApp();
    
  516. 
    
  517.       function overrideContext(path, value) {
    
  518.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  519. 
    
  520.         bridge.send('overrideValueAtPath', {
    
  521.           id,
    
  522.           path,
    
  523.           rendererID,
    
  524.           type: 'context',
    
  525.           value,
    
  526.         });
    
  527.         flushPendingUpdates();
    
  528.       }
    
  529. 
    
  530.       overrideContext(['shallow'], 'updated');
    
  531.       expect(committedContext).toStrictEqual({
    
  532.         array: [1, 2, 3],
    
  533.         object: {
    
  534.           nested: 'initial',
    
  535.         },
    
  536.         shallow: 'updated',
    
  537.       });
    
  538. 
    
  539.       overrideContext(['object', 'nested'], 'updated');
    
  540.       expect(committedContext).toStrictEqual({
    
  541.         array: [1, 2, 3],
    
  542.         object: {
    
  543.           nested: 'updated',
    
  544.         },
    
  545.         shallow: 'updated',
    
  546.       });
    
  547. 
    
  548.       overrideContext(['array', 1], 'updated');
    
  549.       expect(committedContext).toStrictEqual({
    
  550.         array: [1, 'updated', 3],
    
  551.         object: {
    
  552.           nested: 'updated',
    
  553.         },
    
  554.         shallow: 'updated',
    
  555.       });
    
  556.     });
    
  557. 
    
  558.     // @reactVersion >= 16.0
    
  559.     it('should have editable paths', () => {
    
  560.       mountTestApp();
    
  561. 
    
  562.       function renamePath(oldPath, newPath) {
    
  563.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  564. 
    
  565.         bridge.send('renamePath', {
    
  566.           id,
    
  567.           oldPath,
    
  568.           newPath,
    
  569.           rendererID,
    
  570.           type: 'context',
    
  571.         });
    
  572.         flushPendingUpdates();
    
  573.       }
    
  574. 
    
  575.       renamePath(['shallow'], ['after']);
    
  576.       expect(committedContext).toStrictEqual({
    
  577.         array: [1, 2, 3],
    
  578.         object: {
    
  579.           nested: 'initial',
    
  580.         },
    
  581.         after: 'initial',
    
  582.       });
    
  583. 
    
  584.       renamePath(['object', 'nested'], ['object', 'after']);
    
  585.       expect(committedContext).toStrictEqual({
    
  586.         array: [1, 2, 3],
    
  587.         object: {
    
  588.           after: 'initial',
    
  589.         },
    
  590.         after: 'initial',
    
  591.       });
    
  592.     });
    
  593. 
    
  594.     // @reactVersion >= 16.0
    
  595.     it('should enable adding new object properties and array values', async () => {
    
  596.       await mountTestApp();
    
  597. 
    
  598.       function overrideContext(path, value) {
    
  599.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  600. 
    
  601.         bridge.send('overrideValueAtPath', {
    
  602.           id,
    
  603.           path,
    
  604.           rendererID,
    
  605.           type: 'context',
    
  606.           value,
    
  607.         });
    
  608.         flushPendingUpdates();
    
  609.       }
    
  610. 
    
  611.       overrideContext(['new'], 'value');
    
  612.       expect(committedContext).toStrictEqual({
    
  613.         array: [1, 2, 3],
    
  614.         object: {
    
  615.           nested: 'initial',
    
  616.         },
    
  617.         shallow: 'initial',
    
  618.         new: 'value',
    
  619.       });
    
  620. 
    
  621.       overrideContext(['object', 'new'], 'value');
    
  622.       expect(committedContext).toStrictEqual({
    
  623.         array: [1, 2, 3],
    
  624.         object: {
    
  625.           nested: 'initial',
    
  626.           new: 'value',
    
  627.         },
    
  628.         shallow: 'initial',
    
  629.         new: 'value',
    
  630.       });
    
  631. 
    
  632.       overrideContext(['array', 3], 'new value');
    
  633.       expect(committedContext).toStrictEqual({
    
  634.         array: [1, 2, 3, 'new value'],
    
  635.         object: {
    
  636.           nested: 'initial',
    
  637.           new: 'value',
    
  638.         },
    
  639.         shallow: 'initial',
    
  640.         new: 'value',
    
  641.       });
    
  642.     });
    
  643. 
    
  644.     // @reactVersion >= 16.0
    
  645.     it('should have deletable keys', () => {
    
  646.       mountTestApp();
    
  647. 
    
  648.       function deletePath(path) {
    
  649.         const rendererID = ((store.getRendererIDForElement(id): any): number);
    
  650. 
    
  651.         bridge.send('deletePath', {
    
  652.           id,
    
  653.           path,
    
  654.           rendererID,
    
  655.           type: 'context',
    
  656.         });
    
  657.         flushPendingUpdates();
    
  658.       }
    
  659. 
    
  660.       deletePath(['shallow']);
    
  661.       expect(committedContext).toStrictEqual({
    
  662.         array: [1, 2, 3],
    
  663.         object: {
    
  664.           nested: 'initial',
    
  665.         },
    
  666.       });
    
  667. 
    
  668.       deletePath(['object', 'nested']);
    
  669.       expect(committedContext).toStrictEqual({
    
  670.         array: [1, 2, 3],
    
  671.         object: {},
    
  672.       });
    
  673. 
    
  674.       deletePath(['array', 1]);
    
  675.       expect(committedContext).toStrictEqual({
    
  676.         array: [1, 3],
    
  677.         object: {},
    
  678.       });
    
  679.     });
    
  680.   });
    
  681. });