/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import typeof ReactTestRenderer from 'react-test-renderer';
import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';

import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type Store from 'react-devtools-shared/src/devtools/store';

describe('InspectedElement', () => {
  let React;
  let ReactDOM;
  let ReactDOMClient;
  let PropTypes;
  let TestRenderer: ReactTestRenderer;
  let bridge: FrontendBridge;
  let store: Store;
  let utils;

  let BridgeContext;
  let InspectedElementContext;
  let InspectedElementContextController;
  let SettingsContextController;
  let StoreContext;
  let TreeContextController;

  let TestUtilsAct;
  let TestRendererAct;

  let legacyRender;
  let testRendererInstance;

  let ErrorBoundary;
  let errorBoundaryInstance;

  global.IS_REACT_ACT_ENVIRONMENT = true;

  beforeEach(() => {
    utils = require('./utils');
    utils.beforeEachProfiling();

    legacyRender = utils.legacyRender;

    bridge = global.bridge;
    store = global.store;
    store.collapseNodesByDefault = false;

    React = require('react');
    ReactDOM = require('react-dom');
    ReactDOMClient = require('react-dom/client');
    PropTypes = require('prop-types');
    TestUtilsAct = require('internal-test-utils').act;
    TestRenderer = utils.requireTestRenderer();
    TestRendererAct = require('internal-test-utils').act;

    BridgeContext =
      require('react-devtools-shared/src/devtools/views/context').BridgeContext;
    InspectedElementContext =
      require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContext;
    InspectedElementContextController =
      require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContextController;
    SettingsContextController =
      require('react-devtools-shared/src/devtools/views/Settings/SettingsContext').SettingsContextController;
    StoreContext =
      require('react-devtools-shared/src/devtools/views/context').StoreContext;
    TreeContextController =
      require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;

    // Used by inspectElementAtIndex() helper function
    utils.act(() => {
      testRendererInstance = TestRenderer.create(null, {
        isConcurrent: true,
      });
    });

    errorBoundaryInstance = null;

    ErrorBoundary = class extends React.Component {
      state = {error: null};
      componentDidCatch(error) {
        this.setState({error});
      }
      render() {
        errorBoundaryInstance = this;

        if (this.state.error) {
          return null;
        }
        return this.props.children;
      }
    };
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  const Contexts = ({
    children,
    defaultSelectedElementID = null,
    defaultSelectedElementIndex = null,
  }) => (
    <BridgeContext.Provider value={bridge}>
      <StoreContext.Provider value={store}>
        <SettingsContextController>
          <TreeContextController
            defaultSelectedElementID={defaultSelectedElementID}
            defaultSelectedElementIndex={defaultSelectedElementIndex}>
            <React.Suspense fallback="Loading...">
              <InspectedElementContextController>
                {children}
              </InspectedElementContextController>
            </React.Suspense>
          </TreeContextController>
        </SettingsContextController>
      </StoreContext.Provider>
    </BridgeContext.Provider>
  );

  function useInspectedElement() {
    const {inspectedElement} = React.useContext(InspectedElementContext);
    return inspectedElement;
  }

  function useInspectElementPath() {
    const {inspectPaths} = React.useContext(InspectedElementContext);
    return inspectPaths;
  }

  function noop() {}

  async function inspectElementAtIndex(
    index,
    useCustomHook = noop,
    shouldThrow = false,
  ) {
    let didFinish = false;
    let inspectedElement = null;

    function Suspender() {
      useCustomHook();
      inspectedElement = useInspectedElement();
      didFinish = true;
      return null;
    }

    const id = ((store.getElementIDAtIndex(index): any): number);

    await utils.actAsync(() => {
      testRendererInstance.update(
        <ErrorBoundary>
          <Contexts
            defaultSelectedElementID={id}
            defaultSelectedElementIndex={index}>
            <React.Suspense fallback={null}>
              <Suspender id={id} index={index} />
            </React.Suspense>
          </Contexts>
        </ErrorBoundary>,
      );
    }, false);

    if (!shouldThrow) {
      expect(didFinish).toBe(true);
    }

    return inspectedElement;
  }

  it('should inspect the currently selected element', async () => {
    const Example = () => {
      const [count] = React.useState(1);
      return count;
    };

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example a={1} b="abc" />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement).toMatchInlineSnapshot(`
      {
        "context": null,
        "events": undefined,
        "hooks": [
          {
            "hookSource": {
              "columnNumber": "removed by Jest serializer",
              "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
              "functionName": "Example",
              "lineNumber": "removed by Jest serializer",
            },
            "id": 0,
            "isStateEditable": true,
            "name": "State",
            "subHooks": [],
            "value": 1,
          },
        ],
        "id": 2,
        "owners": null,
        "props": {
          "a": 1,
          "b": "abc",
        },
        "rootType": "render()",
        "state": null,
      }
    `);
  });

  it('should have hasLegacyContext flag set to either "true" or "false" depending on which context API is used.', async () => {
    const contextData = {
      bool: true,
    };

    // Legacy Context API.
    class LegacyContextProvider extends React.Component<any> {
      static childContextTypes = {
        bool: PropTypes.bool,
      };
      getChildContext() {
        return contextData;
      }
      render() {
        return this.props.children;
      }
    }
    class LegacyContextConsumer extends React.Component<any> {
      static contextTypes = {
        bool: PropTypes.bool,
      };
      render() {
        return null;
      }
    }

    // Modern Context API
    const BoolContext = React.createContext(contextData.bool);
    BoolContext.displayName = 'BoolContext';

    class ModernContextType extends React.Component<any> {
      static contextType = BoolContext;
      render() {
        return null;
      }
    }

    const ModernContext = React.createContext();
    ModernContext.displayName = 'ModernContext';

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <React.Fragment>
          <LegacyContextProvider>
            <LegacyContextConsumer />
          </LegacyContextProvider>
          <BoolContext.Consumer>{value => null}</BoolContext.Consumer>
          <ModernContextType />
          <ModernContext.Provider value={contextData}>
            <ModernContext.Consumer>{value => null}</ModernContext.Consumer>
          </ModernContext.Provider>
        </React.Fragment>,
        container,
      ),
    );

    const cases = [
      {
        // <LegacyContextConsumer />
        index: 1,
        shouldHaveLegacyContext: true,
      },
      {
        // <BoolContext.Consumer>
        index: 2,
        shouldHaveLegacyContext: false,
      },
      {
        // <ModernContextType />
        index: 3,
        shouldHaveLegacyContext: false,
      },
      {
        // <ModernContext.Consumer>
        index: 5,
        shouldHaveLegacyContext: false,
      },
    ];

    for (let i = 0; i < cases.length; i++) {
      const {index, shouldHaveLegacyContext} = cases[i];

      // HACK: Recreate TestRenderer instance because we rely on default state values
      // from props like defaultSelectedElementID and it's easier to reset here than
      // to read the TreeDispatcherContext and update the selected ID that way.
      // We're testing the inspected values here, not the context wiring, so that's ok.
      utils.withErrorsOrWarningsIgnored(
        ['An update to %s inside a test was not wrapped in act'],
        () => {
          testRendererInstance = TestRenderer.create(null, {
            isConcurrent: true,
          });
        },
      );

      const inspectedElement = await inspectElementAtIndex(index);

      expect(inspectedElement.context).not.toBe(null);
      expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext);
    }
  });

  it('should poll for updates for the currently selected element', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(
      () => legacyRender(<Example a={1} b="abc" />, container),
      false,
    );

    let inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "a": 1,
        "b": "abc",
      }
    `);

    await utils.actAsync(
      () => legacyRender(<Example a={2} b="def" />, container),
      false,
    );

    // TODO (cache)
    // This test only passes if both the check-for-updates poll AND the test renderer.update() call are included below.
    // It seems like either one of the two should be sufficient but:
    // 1. Running only check-for-updates schedules a transition that React never renders.
    // 2. Running only renderer.update() loads stale data (first props)

    // Wait for our check-for-updates poll to get the new data.
    jest.runOnlyPendingTimers();
    await Promise.resolve();

    inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "a": 2,
        "b": "def",
      }
    `);
  });

  it('should not re-render a function with hooks if it did not update since it was last inspected', async () => {
    let targetRenderCount = 0;

    const Wrapper = ({children}) => children;
    const Target = React.memo(props => {
      targetRenderCount++;
      // Even though his hook isn't referenced, it's used to observe backend rendering.
      React.useState(0);
      return null;
    });

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Wrapper>
          <Target a={1} b="abc" />
        </Wrapper>,
        container,
      ),
    );

    targetRenderCount = 0;

    let inspectedElement = await inspectElementAtIndex(1);
    expect(targetRenderCount).toBe(1);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "a": 1,
        "b": "abc",
      }
    `);

    const prevInspectedElement = inspectedElement;

    targetRenderCount = 0;
    inspectedElement = await inspectElementAtIndex(1);
    expect(targetRenderCount).toBe(0);
    expect(inspectedElement).toEqual(prevInspectedElement);

    targetRenderCount = 0;

    await utils.actAsync(
      () =>
        legacyRender(
          <Wrapper>
            <Target a={2} b="def" />
          </Wrapper>,
          container,
        ),
      false,
    );

    // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
    inspectedElement = await inspectElementAtIndex(1);
    expect(targetRenderCount).toBe(2);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "a": 2,
        "b": "def",
      }
    `);
  });

  // See github.com/facebook/react/issues/22241#issuecomment-931299972
  it('should properly recover from a cache miss on the frontend', async () => {
    let targetRenderCount = 0;

    const Wrapper = ({children}) => children;
    const Target = React.memo(props => {
      targetRenderCount++;
      // Even though his hook isn't referenced, it's used to observe backend rendering.
      React.useState(0);
      return null;
    });

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Wrapper>
          <Target a={1} b="abc" />
        </Wrapper>,
        container,
      ),
    );

    targetRenderCount = 0;

    let inspectedElement = await inspectElementAtIndex(1);
    expect(targetRenderCount).toBe(1);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "a": 1,
        "b": "abc",
      }
    `);

    const prevInspectedElement = inspectedElement;

    // This test causes an intermediate error to be logged but we can ignore it.
    jest.spyOn(console, 'error').mockImplementation(() => {});

    // Clear the frontend cache to simulate DevTools being closed and re-opened.
    // The backend still thinks the most recently-inspected element is still cached,
    // so the frontend needs to tell it to resend a full value.
    // We can verify this by asserting that the component is re-rendered again.
    utils.withErrorsOrWarningsIgnored(
      ['An update to %s inside a test was not wrapped in act'],
      () => {
        testRendererInstance = TestRenderer.create(null, {
          isConcurrent: true,
        });
      },
    );

    const {
      clearCacheForTests,
    } = require('react-devtools-shared/src/inspectedElementMutableSource');
    clearCacheForTests();

    targetRenderCount = 0;
    inspectedElement = await inspectElementAtIndex(1);
    expect(targetRenderCount).toBe(1);
    expect(inspectedElement).toEqual(prevInspectedElement);
  });

  it('should temporarily disable console logging when re-running a component to inspect its hooks', async () => {
    let targetRenderCount = 0;

    jest.spyOn(console, 'error').mockImplementation(() => {});
    jest.spyOn(console, 'info').mockImplementation(() => {});
    jest.spyOn(console, 'log').mockImplementation(() => {});
    jest.spyOn(console, 'warn').mockImplementation(() => {});

    const Target = React.memo(props => {
      targetRenderCount++;
      console.error('error');
      console.info('info');
      console.log('log');
      console.warn('warn');
      React.useState(0);
      return null;
    });

    const container = document.createElement('div');
    const root = ReactDOMClient.createRoot(container);
    await utils.actAsync(() => root.render(<Target a={1} b="abc" />));

    expect(targetRenderCount).toBe(1);
    expect(console.error).toHaveBeenCalledTimes(1);
    expect(console.error).toHaveBeenCalledWith('error');
    expect(console.info).toHaveBeenCalledTimes(1);
    expect(console.info).toHaveBeenCalledWith('info');
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledWith('log');
    expect(console.warn).toHaveBeenCalledTimes(1);
    expect(console.warn).toHaveBeenCalledWith('warn');

    const inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement).not.toBe(null);
    expect(targetRenderCount).toBe(2);
    expect(console.error).toHaveBeenCalledTimes(1);
    expect(console.info).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.warn).toHaveBeenCalledTimes(1);
  });

  it('should support simple data types', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          boolean_false={false}
          boolean_true={true}
          infinity={Infinity}
          integer_zero={0}
          integer_one={1}
          float={1.23}
          string="abc"
          string_empty=""
          nan={NaN}
          value_null={null}
          value_undefined={undefined}
        />,
        container,
      ),
    );

    const inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "boolean_false": false,
        "boolean_true": true,
        "float": 1.23,
        "infinity": Infinity,
        "integer_one": 1,
        "integer_zero": 0,
        "nan": NaN,
        "string": "abc",
        "string_empty": "",
        "value_null": null,
        "value_undefined": undefined,
      }
    `);
  });

  it('should support complex data types', async () => {
    const Immutable = require('immutable');

    const Example = () => null;

    const arrayOfArrays = [[['abc', 123, true], []]];
    const div = document.createElement('div');
    const exampleFunction = () => {};
    const exampleDateISO = '2019-12-31T23:42:42.000Z';
    const setShallow = new Set(['abc', 123]);
    const mapShallow = new Map([
      ['name', 'Brian'],
      ['food', 'sushi'],
    ]);
    const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
    const mapOfMaps = new Map([
      ['first', mapShallow],
      ['second', mapShallow],
    ]);
    const objectOfObjects = {
      inner: {string: 'abc', number: 123, boolean: true},
    };
    const objectWithSymbol = {
      [Symbol('name')]: 'hello',
    };
    const typedArray = Int8Array.from([100, -100, 0]);
    const arrayBuffer = typedArray.buffer;
    const dataView = new DataView(arrayBuffer);
    const immutableMap = Immutable.fromJS({
      a: [{hello: 'there'}, 'fixed', true],
      b: 123,
      c: {
        '1': 'xyz',
        xyz: 1,
      },
    });

    class Class {
      anonymousFunction = () => {};
    }
    const instance = new Class();

    const proxyInstance = new Proxy(() => {}, {
      get: function (_, name) {
        return function () {
          return null;
        };
      },
    });

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          anonymous_fn={instance.anonymousFunction}
          array_buffer={arrayBuffer}
          array_of_arrays={arrayOfArrays}
          // eslint-disable-next-line no-undef
          big_int={BigInt(123)}
          bound_fn={exampleFunction.bind(this)}
          data_view={dataView}
          date={new Date(exampleDateISO)}
          fn={exampleFunction}
          html_element={div}
          immutable={immutableMap}
          map={mapShallow}
          map_of_maps={mapOfMaps}
          object_of_objects={objectOfObjects}
          object_with_symbol={objectWithSymbol}
          proxy={proxyInstance}
          react_element={<span />}
          regexp={/abc/giu}
          set={setShallow}
          set_of_sets={setOfSets}
          symbol={Symbol('symbol')}
          typed_array={typedArray}
        />,
        container,
      ),
    );

    const inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "anonymous_fn": Dehydrated {
          "preview_short": ƒ () {},
          "preview_long": ƒ () {},
        },
        "array_buffer": Dehydrated {
          "preview_short": ArrayBuffer(3),
          "preview_long": ArrayBuffer(3),
        },
        "array_of_arrays": [
          Dehydrated {
            "preview_short": Array(2),
            "preview_long": [Array(3), Array(0)],
          },
        ],
        "big_int": Dehydrated {
          "preview_short": 123n,
          "preview_long": 123n,
        },
        "bound_fn": Dehydrated {
          "preview_short": ƒ bound exampleFunction() {},
          "preview_long": ƒ bound exampleFunction() {},
        },
        "data_view": Dehydrated {
          "preview_short": DataView(3),
          "preview_long": DataView(3),
        },
        "date": Dehydrated {
          "preview_short": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time),
          "preview_long": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time),
        },
        "fn": Dehydrated {
          "preview_short": ƒ exampleFunction() {},
          "preview_long": ƒ exampleFunction() {},
        },
        "html_element": Dehydrated {
          "preview_short": <div />,
          "preview_long": <div />,
        },
        "immutable": {
          "0": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["a", List(3)],
          },
          "1": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["b", 123],
          },
          "2": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["c", Map(2)],
          },
        },
        "map": {
          "0": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["name", "Brian"],
          },
          "1": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["food", "sushi"],
          },
        },
        "map_of_maps": {
          "0": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["first", Map(2)],
          },
          "1": Dehydrated {
            "preview_short": Array(2),
            "preview_long": ["second", Map(2)],
          },
        },
        "object_of_objects": {
          "inner": Dehydrated {
            "preview_short": {…},
            "preview_long": {boolean: true, number: 123, string: "abc"},
          },
        },
        "object_with_symbol": {
          "Symbol(name)": "hello",
        },
        "proxy": Dehydrated {
          "preview_short": ƒ () {},
          "preview_long": ƒ () {},
        },
        "react_element": Dehydrated {
          "preview_short": <span />,
          "preview_long": <span />,
        },
        "regexp": Dehydrated {
          "preview_short": /abc/giu,
          "preview_long": /abc/giu,
        },
        "set": {
          "0": "abc",
          "1": 123,
        },
        "set_of_sets": {
          "0": Dehydrated {
            "preview_short": Set(3),
            "preview_long": Set(3) {"a", "b", "c"},
          },
          "1": Dehydrated {
            "preview_short": Set(3),
            "preview_long": Set(3) {1, 2, 3},
          },
        },
        "symbol": Dehydrated {
          "preview_short": Symbol(symbol),
          "preview_long": Symbol(symbol),
        },
        "typed_array": {
          "0": 100,
          "1": -100,
          "2": 0,
        },
      }
    `);
  });

  it('should not consume iterables while inspecting', async () => {
    const Example = () => null;

    function* generator() {
      throw Error('Should not be consumed!');
    }

    const container = document.createElement('div');

    const iterable = generator();
    await utils.actAsync(() =>
      legacyRender(<Example prop={iterable} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "prop": Dehydrated {
          "preview_short": Generator,
          "preview_long": Generator,
        },
      }
    `);
  });

  it('should support objects with no prototype', async () => {
    const Example = () => null;

    const object = Object.create(null);
    object.string = 'abc';
    object.number = 123;
    object.boolean = true;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example object={object} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "object": {
          "boolean": true,
          "number": 123,
          "string": "abc",
        },
      }
    `);
  });

  it('should support objects with overridden hasOwnProperty', async () => {
    const Example = () => null;

    const object = {
      name: 'blah',
      hasOwnProperty: true,
    };

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example object={object} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "object": {
          "hasOwnProperty": true,
          "name": "blah",
        },
      }
    `);
  });

  it('should support custom objects with enumerable properties and getters', async () => {
    class CustomData {
      _number = 42;
      get number() {
        return this._number;
      }
      set number(value) {
        this._number = value;
      }
    }

    const descriptor = ((Object.getOwnPropertyDescriptor(
      CustomData.prototype,
      'number',
    ): any): PropertyDescriptor<number>);
    descriptor.enumerable = true;
    Object.defineProperty(CustomData.prototype, 'number', descriptor);

    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example data={new CustomData()} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "data": {
          "_number": 42,
          "number": 42,
        },
      }
    `);
  });

  it('should support objects with inherited keys', async () => {
    const Example = () => null;

    const base = Object.create(Object.prototype, {
      enumerableStringBase: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true,
      },
      [Symbol('enumerableSymbolBase')]: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true,
      },
      nonEnumerableStringBase: {
        value: 1,
        writable: true,
        enumerable: false,
        configurable: true,
      },
      [Symbol('nonEnumerableSymbolBase')]: {
        value: 1,
        writable: true,
        enumerable: false,
        configurable: true,
      },
    });

    const object = Object.create(base, {
      enumerableString: {
        value: 2,
        writable: true,
        enumerable: true,
        configurable: true,
      },
      nonEnumerableString: {
        value: 3,
        writable: true,
        enumerable: false,
        configurable: true,
      },
      123: {
        value: 3,
        writable: true,
        enumerable: true,
        configurable: true,
      },
      [Symbol('nonEnumerableSymbol')]: {
        value: 2,
        writable: true,
        enumerable: false,
        configurable: true,
      },
      [Symbol('enumerableSymbol')]: {
        value: 3,
        writable: true,
        enumerable: true,
        configurable: true,
      },
    });

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example object={object} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "object": {
          "123": 3,
          "Symbol(enumerableSymbol)": 3,
          "Symbol(enumerableSymbolBase)": 1,
          "enumerableString": 2,
          "enumerableStringBase": 1,
        },
      }
    `);
  });

  it('should allow component prop value and value`s prototype has same name params.', async () => {
    const testData = Object.create(
      {
        a: undefined,
        b: Infinity,
        c: NaN,
        d: 'normal',
      },
      {
        a: {
          value: undefined,
          writable: true,
          enumerable: true,
          configurable: true,
        },
        b: {
          value: Infinity,
          writable: true,
          enumerable: true,
          configurable: true,
        },
        c: {
          value: NaN,
          writable: true,
          enumerable: true,
          configurable: true,
        },
        d: {
          value: 'normal',
          writable: true,
          enumerable: true,
          configurable: true,
        },
      },
    );
    const Example = ({data}) => null;
    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example data={testData} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "data": {
          "a": undefined,
          "b": Infinity,
          "c": NaN,
          "d": "normal",
        },
      }
    `);
  });

  it('should not dehydrate nested values until explicitly requested', async () => {
    const Example = () => {
      const [state] = React.useState({
        foo: {
          bar: {
            baz: 'hi',
          },
        },
      });

      return state.foo.bar.baz;
    };

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          nestedObject={{
            a: {
              b: {
                c: [
                  {
                    d: {
                      e: {},
                    },
                  },
                ],
              },
            },
          }}
        />,
        container,
      ),
    );

    let inspectedElement = null;
    let inspectElementPath = null;

    // Render once to get a handle on inspectElementPath()
    inspectedElement = await inspectElementAtIndex(0, () => {
      inspectElementPath = useInspectElementPath();
    });

    async function loadPath(path) {
      await TestUtilsAct(async () => {
        await TestRendererAct(async () => {
          inspectElementPath(path);
        });
      });

      inspectedElement = await inspectElementAtIndex(0);
    }

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": Dehydrated {
            "preview_short": {…},
            "preview_long": {b: {…}},
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'a']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "c": Dehydrated {
                "preview_short": Array(1),
                "preview_long": [{…}],
              },
            },
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'a', 'b', 'c']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "c": [
                {
                  "d": Dehydrated {
                    "preview_short": {…},
                    "preview_long": {e: {…}},
                  },
                },
              ],
            },
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "c": [
                {
                  "d": {
                    "e": {},
                  },
                },
              ],
            },
          },
        },
      }
    `);

    await loadPath(['hooks', 0, 'value']);

    expect(inspectedElement.hooks).toMatchInlineSnapshot(`
      [
        {
          "hookSource": {
            "columnNumber": "removed by Jest serializer",
            "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
            "functionName": "Example",
            "lineNumber": "removed by Jest serializer",
          },
          "id": 0,
          "isStateEditable": true,
          "name": "State",
          "subHooks": [],
          "value": {
            "foo": {
              "bar": Dehydrated {
                "preview_short": {…},
                "preview_long": {baz: "hi"},
              },
            },
          },
        },
      ]
    `);

    await loadPath(['hooks', 0, 'value', 'foo', 'bar']);

    expect(inspectedElement.hooks).toMatchInlineSnapshot(`
      [
        {
          "hookSource": {
            "columnNumber": "removed by Jest serializer",
            "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
            "functionName": "Example",
            "lineNumber": "removed by Jest serializer",
          },
          "id": 0,
          "isStateEditable": true,
          "name": "State",
          "subHooks": [],
          "value": {
            "foo": {
              "bar": {
                "baz": "hi",
              },
            },
          },
        },
      ]
    `);
  });

  it('should dehydrate complex nested values when requested', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
        />,
        container,
      ),
    );

    let inspectedElement = null;
    let inspectElementPath = null;

    // Render once to get a handle on inspectElementPath()
    inspectedElement = await inspectElementAtIndex(0, () => {
      inspectElementPath = useInspectElementPath();
    });

    async function loadPath(path) {
      await TestUtilsAct(async () => {
        await TestRendererAct(async () => {
          inspectElementPath(path);
        });
      });

      inspectedElement = await inspectElementAtIndex(0);
    }

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "set_of_sets": {
          "0": Dehydrated {
            "preview_short": Set(3),
            "preview_long": Set(3) {1, 2, 3},
          },
          "1": Dehydrated {
            "preview_short": Set(3),
            "preview_long": Set(3) {"a", "b", "c"},
          },
        },
      }
    `);

    await loadPath(['props', 'set_of_sets', 0]);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "set_of_sets": {
          "0": {
            "0": 1,
            "1": 2,
            "2": 3,
          },
          "1": Dehydrated {
            "preview_short": Set(3),
            "preview_long": Set(3) {"a", "b", "c"},
          },
        },
      }
    `);
  });

  it('should include updates for nested values that were previously hydrated', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          nestedObject={{
            a: {
              value: 1,
              b: {
                value: 1,
              },
            },
            c: {
              value: 1,
              d: {
                value: 1,
                e: {
                  value: 1,
                },
              },
            },
          }}
        />,
        container,
      ),
    );

    let inspectedElement = null;
    let inspectElementPath = null;

    // Render once to get a handle on inspectElementPath()
    inspectedElement = await inspectElementAtIndex(0, () => {
      inspectElementPath = useInspectElementPath();
    });

    async function loadPath(path) {
      await TestUtilsAct(async () => {
        await TestRendererAct(async () => {
          inspectElementPath(path);
        });
      });

      inspectedElement = await inspectElementAtIndex(0);
    }

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": Dehydrated {
            "preview_short": {…},
            "preview_long": {b: {…}, value: 1},
          },
          "c": Dehydrated {
            "preview_short": {…},
            "preview_long": {d: {…}, value: 1},
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'a']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 1,
            },
            "value": 1,
          },
          "c": Dehydrated {
            "preview_short": {…},
            "preview_long": {d: {…}, value: 1},
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'c']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 1,
            },
            "value": 1,
          },
          "c": {
            "d": {
              "e": Dehydrated {
                "preview_short": {…},
                "preview_long": {value: 1},
              },
              "value": 1,
            },
            "value": 1,
          },
        },
      }
    `);

    await TestRendererAct(async () => {
      await TestUtilsAct(async () => {
        legacyRender(
          <Example
            nestedObject={{
              a: {
                value: 2,
                b: {
                  value: 2,
                },
              },
              c: {
                value: 2,
                d: {
                  value: 2,
                  e: {
                    value: 2,
                  },
                },
              },
            }}
          />,
          container,
        );
      });
    });

    // Wait for pending poll-for-update and then update inspected element data.
    jest.runOnlyPendingTimers();
    await Promise.resolve();
    inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 2,
            },
            "value": 2,
          },
          "c": {
            "d": {
              "e": Dehydrated {
                "preview_short": {…},
                "preview_long": {value: 2},
              },
              "value": 2,
            },
            "value": 2,
          },
        },
      }
    `);
  });

  it('should return a full update if a path is inspected for an object that has other pending changes', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          nestedObject={{
            a: {
              value: 1,
              b: {
                value: 1,
              },
            },
            c: {
              value: 1,
              d: {
                value: 1,
                e: {
                  value: 1,
                },
              },
            },
          }}
        />,
        container,
      ),
    );

    let inspectedElement = null;
    let inspectElementPath = null;

    // Render once to get a handle on inspectElementPath()
    inspectedElement = await inspectElementAtIndex(0, () => {
      inspectElementPath = useInspectElementPath();
    });

    async function loadPath(path) {
      await TestUtilsAct(async () => {
        await TestRendererAct(() => {
          inspectElementPath(path);
        });
      });

      inspectedElement = await inspectElementAtIndex(0);
    }

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": Dehydrated {
            "preview_short": {…},
            "preview_long": {b: {…}, value: 1},
          },
          "c": Dehydrated {
            "preview_short": {…},
            "preview_long": {d: {…}, value: 1},
          },
        },
      }
    `);

    await loadPath(['props', 'nestedObject', 'a']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 1,
            },
            "value": 1,
          },
          "c": Dehydrated {
            "preview_short": {…},
            "preview_long": {d: {…}, value: 1},
          },
        },
      }
    `);

    await TestRendererAct(async () => {
      await TestUtilsAct(async () => {
        legacyRender(
          <Example
            nestedObject={{
              a: {
                value: 2,
                b: {
                  value: 2,
                },
              },
              c: {
                value: 2,
                d: {
                  value: 2,
                  e: {
                    value: 2,
                  },
                },
              },
            }}
          />,
          container,
        );
      });
    });

    await loadPath(['props', 'nestedObject', 'c']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 2,
            },
            "value": 2,
          },
          "c": {
            "d": {
              "e": Dehydrated {
                "preview_short": {…},
                "preview_long": {value: 2},
              },
              "value": 2,
            },
            "value": 2,
          },
        },
      }
    `);
  });

  it('should not tear if hydration is requested after an update', async () => {
    const Example = () => null;

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(
        <Example
          nestedObject={{
            value: 1,
            a: {
              value: 1,
              b: {
                value: 1,
              },
            },
          }}
        />,
        container,
      ),
    );

    let inspectedElement = null;
    let inspectElementPath = null;

    // Render once to get a handle on inspectElementPath()
    inspectedElement = await inspectElementAtIndex(0, () => {
      inspectElementPath = useInspectElementPath();
    });

    async function loadPath(path) {
      await TestUtilsAct(async () => {
        await TestRendererAct(() => {
          inspectElementPath(path);
        });
      });

      inspectedElement = await inspectElementAtIndex(0);
    }

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": Dehydrated {
            "preview_short": {…},
            "preview_long": {b: {…}, value: 1},
          },
          "value": 1,
        },
      }
    `);

    await TestUtilsAct(async () => {
      legacyRender(
        <Example
          nestedObject={{
            value: 2,
            a: {
              value: 2,
              b: {
                value: 2,
              },
            },
          }}
        />,
        container,
      );
    });

    await loadPath(['props', 'nestedObject', 'a']);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "nestedObject": {
          "a": {
            "b": {
              "value": 2,
            },
            "value": 2,
          },
          "value": 2,
        },
      }
    `);
  });

  it('should inspect hooks for components that only use context', async () => {
    const Context = React.createContext(true);
    const Example = () => {
      const value = React.useContext(Context);
      return value;
    };

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example a={1} b="abc" />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement).toMatchInlineSnapshot(`
      {
        "context": null,
        "events": undefined,
        "hooks": [
          {
            "hookSource": {
              "columnNumber": "removed by Jest serializer",
              "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
              "functionName": "Example",
              "lineNumber": "removed by Jest serializer",
            },
            "id": null,
            "isStateEditable": false,
            "name": "Context",
            "subHooks": [],
            "value": true,
          },
        ],
        "id": 2,
        "owners": null,
        "props": {
          "a": 1,
          "b": "abc",
        },
        "rootType": "render()",
        "state": null,
      }
    `);
  });

  it('should enable inspected values to be stored as global variables', async () => {
    const Example = () => null;

    const nestedObject = {
      a: {
        value: 1,
        b: {
          value: 1,
          c: {
            value: 1,
          },
        },
      },
    };

    await utils.actAsync(() =>
      legacyRender(
        <Example nestedObject={nestedObject} />,
        document.createElement('div'),
      ),
    );

    let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);

    const id = ((store.getElementIDAtIndex(0): any): number);
    await inspectElementAtIndex(0, () => {
      storeAsGlobal = (path: Array<string | number>) => {
        const rendererID = store.getRendererIDForElement(id);
        if (rendererID !== null) {
          const {
            storeAsGlobal: storeAsGlobalAPI,
          } = require('react-devtools-shared/src/backendAPI');
          storeAsGlobalAPI({
            bridge,
            id,
            path,
            rendererID,
          });
        }
      };
    });

    jest.spyOn(console, 'log').mockImplementation(() => {});

    // Should store the whole value (not just the hydrated parts)
    storeAsGlobal(['props', 'nestedObject']);
    jest.runOnlyPendingTimers();
    expect(console.log).toHaveBeenCalledWith('$reactTemp0');
    expect(global.$reactTemp0).toBe(nestedObject);

    console.log.mockReset();

    // Should store the nested property specified (not just the outer value)
    storeAsGlobal(['props', 'nestedObject', 'a', 'b']);
    jest.runOnlyPendingTimers();
    expect(console.log).toHaveBeenCalledWith('$reactTemp1');
    expect(global.$reactTemp1).toBe(nestedObject.a.b);
  });

  it('should enable inspected values to be copied to the clipboard', async () => {
    const Example = () => null;

    const nestedObject = {
      a: {
        value: 1,
        b: {
          value: 1,
          c: {
            value: 1,
          },
        },
      },
    };

    await utils.actAsync(() =>
      legacyRender(
        <Example nestedObject={nestedObject} />,
        document.createElement('div'),
      ),
    );

    let copyPath: CopyInspectedElementPath =
      ((null: any): CopyInspectedElementPath);

    const id = ((store.getElementIDAtIndex(0): any): number);
    await inspectElementAtIndex(0, () => {
      copyPath = (path: Array<string | number>) => {
        const rendererID = store.getRendererIDForElement(id);
        if (rendererID !== null) {
          const {
            copyInspectedElementPath,
          } = require('react-devtools-shared/src/backendAPI');
          copyInspectedElementPath({
            bridge,
            id,
            path,
            rendererID,
          });
        }
      };
    });

    // Should copy the whole value (not just the hydrated parts)
    copyPath(['props', 'nestedObject']);
    jest.runOnlyPendingTimers();
    expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
    expect(global.mockClipboardCopy).toHaveBeenCalledWith(
      JSON.stringify(nestedObject, undefined, 2),
    );

    global.mockClipboardCopy.mockReset();

    // Should copy the nested property specified (not just the outer value)
    copyPath(['props', 'nestedObject', 'a', 'b']);
    jest.runOnlyPendingTimers();
    expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
    expect(global.mockClipboardCopy).toHaveBeenCalledWith(
      JSON.stringify(nestedObject.a.b, undefined, 2),
    );
  });

  it('should enable complex values to be copied to the clipboard', async () => {
    const Immutable = require('immutable');

    const Example = () => null;

    const set = new Set(['abc', 123]);
    const map = new Map([
      ['name', 'Brian'],
      ['food', 'sushi'],
    ]);
    const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
    const mapOfMaps = new Map([
      ['first', map],
      ['second', map],
    ]);
    const typedArray = Int8Array.from([100, -100, 0]);
    const arrayBuffer = typedArray.buffer;
    const dataView = new DataView(arrayBuffer);
    const immutable = Immutable.fromJS({
      a: [{hello: 'there'}, 'fixed', true],
      b: 123,
      c: {
        '1': 'xyz',
        xyz: 1,
      },
    });
    const bigInt = BigInt(123); // eslint-disable-line no-undef

    await utils.actAsync(() =>
      legacyRender(
        <Example
          arrayBuffer={arrayBuffer}
          dataView={dataView}
          map={map}
          set={set}
          mapOfMaps={mapOfMaps}
          setOfSets={setOfSets}
          typedArray={typedArray}
          immutable={immutable}
          bigInt={bigInt}
        />,
        document.createElement('div'),
      ),
    );

    const id = ((store.getElementIDAtIndex(0): any): number);

    let copyPath: CopyInspectedElementPath =
      ((null: any): CopyInspectedElementPath);

    await inspectElementAtIndex(0, () => {
      copyPath = (path: Array<string | number>) => {
        const rendererID = store.getRendererIDForElement(id);
        if (rendererID !== null) {
          const {
            copyInspectedElementPath,
          } = require('react-devtools-shared/src/backendAPI');
          copyInspectedElementPath({
            bridge,
            id,
            path,
            rendererID,
          });
        }
      };
    });

    // Should copy the whole value (not just the hydrated parts)
    copyPath(['props']);
    jest.runOnlyPendingTimers();
    // Should not error despite lots of unserialized values.

    global.mockClipboardCopy.mockReset();

    // Should copy the nested property specified (not just the outer value)
    copyPath(['props', 'bigInt']);
    jest.runOnlyPendingTimers();
    expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
    expect(global.mockClipboardCopy).toHaveBeenCalledWith(
      JSON.stringify('123n', undefined, 2),
    );

    global.mockClipboardCopy.mockReset();

    // Should copy the nested property specified (not just the outer value)
    copyPath(['props', 'typedArray']);
    jest.runOnlyPendingTimers();
    expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
    expect(global.mockClipboardCopy).toHaveBeenCalledWith(
      JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
    );
  });

  it('should display complex values of useDebugValue', async () => {
    const container = document.createElement('div');

    function useDebuggableHook() {
      React.useDebugValue({foo: 2});
      React.useState(1);
      return 1;
    }
    function DisplayedComplexValue() {
      useDebuggableHook();
      return null;
    }

    await utils.actAsync(() =>
      legacyRender(<DisplayedComplexValue />, container),
    );

    const {hooks} = await inspectElementAtIndex(0);
    expect(hooks).toMatchInlineSnapshot(`
      [
        {
          "hookSource": {
            "columnNumber": "removed by Jest serializer",
            "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
            "functionName": "DisplayedComplexValue",
            "lineNumber": "removed by Jest serializer",
          },
          "id": null,
          "isStateEditable": false,
          "name": "DebuggableHook",
          "subHooks": [
            {
              "hookSource": {
                "columnNumber": "removed by Jest serializer",
                "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
                "functionName": "useDebuggableHook",
                "lineNumber": "removed by Jest serializer",
              },
              "id": 0,
              "isStateEditable": true,
              "name": "State",
              "subHooks": [],
              "value": 1,
            },
          ],
          "value": {
            "foo": 2,
          },
        },
      ]
    `);
  });

  // See github.com/facebook/react/issues/21654
  it('should support Proxies that dont return an iterator', async () => {
    const Example = () => null;
    const proxy = new Proxy(
      {},
      {
        get: (target, prop, receiver) => {
          target[prop] = value => {};
          return target[prop];
        },
      },
    );

    const container = document.createElement('div');
    await utils.actAsync(() =>
      legacyRender(<Example proxy={proxy} />, container),
    );

    const inspectedElement = await inspectElementAtIndex(0);

    expect(inspectedElement.props).toMatchInlineSnapshot(`
      {
        "proxy": {
          "$$typeof": Dehydrated {
            "preview_short": ƒ () {},
            "preview_long": ƒ () {},
          },
          "Symbol(Symbol.iterator)": Dehydrated {
            "preview_short": ƒ () {},
            "preview_long": ƒ () {},
          },
          "constructor": Dehydrated {
            "preview_short": ƒ () {},
            "preview_long": ƒ () {},
          },
        },
      }
    `);
  });

  // Regression test for github.com/facebook/react/issues/22099
  it('should not error when an unchanged component is re-inspected after component filters changed', async () => {
    const Example = () => <div />;

    const container = document.createElement('div');
    await utils.actAsync(() => legacyRender(<Example />, container));

    // Select/inspect element
    let inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement).toMatchInlineSnapshot(`
      {
        "context": null,
        "events": undefined,
        "hooks": null,
        "id": 2,
        "owners": null,
        "props": {},
        "rootType": "render()",
        "state": null,
      }
    `);

    await utils.actAsync(async () => {
      // Ignore transient warning this causes
      utils.withErrorsOrWarningsIgnored(['No element found with id'], () => {
        store.componentFilters = [];

        // Flush events to the renderer.
        jest.runOnlyPendingTimers();
      });
    }, false);

    // HACK: Recreate TestRenderer instance because we rely on default state values
    // from props like defaultSelectedElementID and it's easier to reset here than
    // to read the TreeDispatcherContext and update the selected ID that way.
    // We're testing the inspected values here, not the context wiring, so that's ok.
    utils.withErrorsOrWarningsIgnored(
      ['An update to %s inside a test was not wrapped in act'],
      () => {
        testRendererInstance = TestRenderer.create(null, {
          isConcurrent: true,
        });
      },
    );

    // Select/inspect the same element again
    inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement).toMatchInlineSnapshot(`
      {
        "context": null,
        "events": undefined,
        "hooks": null,
        "id": 2,
        "owners": null,
        "props": {},
        "rootType": "render()",
        "state": null,
      }
    `);
  });

  it('should display the root type for ReactDOM.hydrate', async () => {
    const Example = () => <div />;

    await utils.actAsync(() => {
      const container = document.createElement('div');
      container.innerHTML = '<div></div>';
      withErrorsOrWarningsIgnored(
        ['ReactDOM.hydrate is no longer supported in React 18'],
        () => {
          ReactDOM.hydrate(<Example />, container);
        },
      );
    }, false);

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.rootType).toMatchInlineSnapshot(`"hydrate()"`);
  });

  it('should display the root type for ReactDOM.render', async () => {
    const Example = () => <div />;

    await utils.actAsync(() => {
      const container = document.createElement('div');
      legacyRender(<Example />, container);
    }, false);

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.rootType).toMatchInlineSnapshot(`"render()"`);
  });

  it('should display the root type for ReactDOMClient.hydrateRoot', async () => {
    const Example = () => <div />;

    await utils.actAsync(() => {
      const container = document.createElement('div');
      container.innerHTML = '<div></div>';
      ReactDOMClient.hydrateRoot(container, <Example />);
    }, false);

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.rootType).toMatchInlineSnapshot(`"hydrateRoot()"`);
  });

  it('should display the root type for ReactDOMClient.createRoot', async () => {
    const Example = () => <div />;

    await utils.actAsync(() => {
      const container = document.createElement('div');
      ReactDOMClient.createRoot(container).render(<Example />);
    }, false);

    const inspectedElement = await inspectElementAtIndex(0);
    expect(inspectedElement.rootType).toMatchInlineSnapshot(`"createRoot()"`);
  });

  it('should gracefully surface backend errors on the frontend rather than timing out', async () => {
    jest.spyOn(console, 'error').mockImplementation(() => {});

    let shouldThrow = false;

    const Example = () => {
      const [count] = React.useState(0);

      if (shouldThrow) {
        throw Error('Expected');
      } else {
        return count;
      }
    };

    await utils.actAsync(() => {
      const container = document.createElement('div');
      ReactDOMClient.createRoot(container).render(<Example />);
    }, false);

    shouldThrow = true;

    const value = await inspectElementAtIndex(0, noop, true);

    expect(value).toBe(null);

    const error = errorBoundaryInstance.state.error;
    expect(error.message).toBe('Expected');
    expect(error.stack).toContain('inspectHooksOfFiber');
  });

  describe('$r', () => {
    it('should support function components', async () => {
      const Example = () => {
        const [count] = React.useState(1);
        return count;
      };

      const container = document.createElement('div');
      await utils.actAsync(() =>
        legacyRender(<Example a={1} b="abc" />, container),
      );

      await inspectElementAtIndex(0);

      expect(global.$r).toMatchInlineSnapshot(`
        {
          "hooks": [
            {
              "hookSource": {
                "columnNumber": "removed by Jest serializer",
                "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
                "functionName": "Example",
                "lineNumber": "removed by Jest serializer",
              },
              "id": 0,
              "isStateEditable": true,
              "name": "State",
              "subHooks": [],
              "value": 1,
            },
          ],
          "props": {
            "a": 1,
            "b": "abc",
          },
          "type": [Function],
        }
      `);
    });

    it('should support memoized function components', async () => {
      const Example = React.memo(function Example(props) {
        const [count] = React.useState(1);
        return count;
      });

      const container = document.createElement('div');
      await utils.actAsync(() =>
        legacyRender(<Example a={1} b="abc" />, container),
      );

      await inspectElementAtIndex(0);

      expect(global.$r).toMatchInlineSnapshot(`
        {
          "hooks": [
            {
              "hookSource": {
                "columnNumber": "removed by Jest serializer",
                "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
                "functionName": "Example",
                "lineNumber": "removed by Jest serializer",
              },
              "id": 0,
              "isStateEditable": true,
              "name": "State",
              "subHooks": [],
              "value": 1,
            },
          ],
          "props": {
            "a": 1,
            "b": "abc",
          },
          "type": [Function],
        }
      `);
    });

    it('should support forward refs', async () => {
      const Example = React.forwardRef(function Example(props, ref) {
        const [count] = React.useState(1);
        return count;
      });

      const container = document.createElement('div');
      await utils.actAsync(() =>
        legacyRender(<Example a={1} b="abc" />, container),
      );

      await inspectElementAtIndex(0);

      expect(global.$r).toMatchInlineSnapshot(`
        {
          "hooks": [
            {
              "hookSource": {
                "columnNumber": "removed by Jest serializer",
                "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
                "functionName": "Example",
                "lineNumber": "removed by Jest serializer",
              },
              "id": 0,
              "isStateEditable": true,
              "name": "State",
              "subHooks": [],
              "value": 1,
            },
          ],
          "props": {
            "a": 1,
            "b": "abc",
          },
          "type": [Function],
        }
      `);
    });

    it('should support class components', async () => {
      class Example extends React.Component {
        state = {
          count: 0,
        };
        render() {
          return null;
        }
      }

      const container = document.createElement('div');
      await utils.actAsync(() =>
        legacyRender(<Example a={1} b="abc" />, container),
      );

      await inspectElementAtIndex(0);

      expect(global.$r.props).toMatchInlineSnapshot(`
              {
                "a": 1,
                "b": "abc",
              }
            `);
      expect(global.$r.state).toMatchInlineSnapshot(`
              {
                "count": 0,
              }
            `);
    });
  });

  describe('inline errors and warnings', () => {
    async function getErrorsAndWarningsForElementAtIndex(index) {
      const id = ((store.getElementIDAtIndex(index): any): number);
      if (id == null) {
        throw Error(`Element at index "${index}"" not found in store`);
      }

      let errors = null;
      let warnings = null;

      function Suspender({target}) {
        const inspectedElement = useInspectedElement();
        errors = inspectedElement.errors;
        warnings = inspectedElement.warnings;
        return null;
      }

      let root;
      await utils.actAsync(() => {
        root = TestRenderer.create(
          <Contexts
            defaultSelectedElementID={id}
            defaultSelectedElementIndex={index}>
            <React.Suspense fallback={null}>
              <Suspender target={id} />
            </React.Suspense>
          </Contexts>,
          {isConcurrent: true},
        );
      }, false);
      await utils.actAsync(() => {
        root.unmount();
      }, false);

      return {errors, warnings};
    }

    it('during render get recorded', async () => {
      const Example = () => {
        console.error('test-only: render error');
        console.warn('test-only: render warning');
        return null;
      };

      const container = document.createElement('div');

      await withErrorsOrWarningsIgnored(['test-only: '], async () => {
        await utils.actAsync(() =>
          legacyRender(<Example repeatWarningCount={1} />, container),
        );
      });

      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [
            [
              "test-only: render error",
              1,
            ],
          ],
          "warnings": [
            [
              "test-only: render warning",
              1,
            ],
          ],
        }
      `);
    });

    it('during render get deduped', async () => {
      const Example = () => {
        console.error('test-only: render error');
        console.error('test-only: render error');
        console.warn('test-only: render warning');
        console.warn('test-only: render warning');
        console.warn('test-only: render warning');
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(<Example repeatWarningCount={1} />, container),
        );
      });
      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [
            [
              "test-only: render error",
              2,
            ],
          ],
          "warnings": [
            [
              "test-only: render warning",
              3,
            ],
          ],
        }
      `);
    });

    it('during layout (mount) get recorded', async () => {
      const Example = () => {
        // Note we only test mount because once the component unmounts,
        // it is no longer in the store and warnings are ignored.
        React.useLayoutEffect(() => {
          console.error('test-only: useLayoutEffect error');
          console.warn('test-only: useLayoutEffect warning');
        }, []);
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(<Example repeatWarningCount={1} />, container),
        );
      });

      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [
            [
              "test-only: useLayoutEffect error",
              1,
            ],
          ],
          "warnings": [
            [
              "test-only: useLayoutEffect warning",
              1,
            ],
          ],
        }
      `);
    });

    it('during passive (mount) get recorded', async () => {
      const Example = () => {
        // Note we only test mount because once the component unmounts,
        // it is no longer in the store and warnings are ignored.
        React.useEffect(() => {
          console.error('test-only: useEffect error');
          console.warn('test-only: useEffect warning');
        }, []);
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(<Example repeatWarningCount={1} />, container),
        );
      });

      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [
            [
              "test-only: useEffect error",
              1,
            ],
          ],
          "warnings": [
            [
              "test-only: useEffect warning",
              1,
            ],
          ],
        }
      `);
    });

    it('from react get recorded without a component stack', async () => {
      const Example = () => {
        return [<div />];
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(
        ['Warning: Each child in a list should have a unique "key" prop.'],
        async () => {
          await utils.actAsync(() =>
            legacyRender(<Example repeatWarningCount={1} />, container),
          );
        },
      );

      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [
            [
              "Warning: Each child in a list should have a unique "key" prop. See https://reactjs.org/link/warning-keys for more information.
            at Example",
              1,
            ],
          ],
          "warnings": [],
        }
      `);
    });

    it('can be cleared for the whole app', async () => {
      const Example = () => {
        console.error('test-only: render error');
        console.warn('test-only: render warning');
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(<Example repeatWarningCount={1} />, container),
        );
      });

      const {
        clearErrorsAndWarnings,
      } = require('react-devtools-shared/src/backendAPI');
      clearErrorsAndWarnings({bridge, store});

      // Flush events to the renderer.
      jest.runOnlyPendingTimers();

      const data = await getErrorsAndWarningsForElementAtIndex(0);
      expect(data).toMatchInlineSnapshot(`
        {
          "errors": [],
          "warnings": [],
        }
      `);
    });

    it('can be cleared for a particular Fiber (only warnings)', async () => {
      const Example = ({id}) => {
        console.error(`test-only: render error #${id}`);
        console.warn(`test-only: render warning #${id}`);
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(
            <React.Fragment>
              <Example id={1} />
              <Example id={2} />
            </React.Fragment>,
            container,
          ),
        );
      });

      let id = ((store.getElementIDAtIndex(1): any): number);
      const rendererID = store.getRendererIDForElement(id);

      const {
        clearWarningsForElement,
      } = require('react-devtools-shared/src/backendAPI');
      clearWarningsForElement({bridge, id, rendererID});

      // Flush events to the renderer.
      jest.runOnlyPendingTimers();

      let data = [
        await getErrorsAndWarningsForElementAtIndex(0),
        await getErrorsAndWarningsForElementAtIndex(1),
      ];
      expect(data).toMatchInlineSnapshot(`
        [
          {
            "errors": [
              [
                "test-only: render error #1",
                1,
              ],
            ],
            "warnings": [
              [
                "test-only: render warning #1",
                1,
              ],
            ],
          },
          {
            "errors": [
              [
                "test-only: render error #2",
                1,
              ],
            ],
            "warnings": [],
          },
        ]
      `);

      id = ((store.getElementIDAtIndex(0): any): number);
      clearWarningsForElement({bridge, id, rendererID});

      // Flush events to the renderer.
      jest.runOnlyPendingTimers();

      data = [
        await getErrorsAndWarningsForElementAtIndex(0),
        await getErrorsAndWarningsForElementAtIndex(1),
      ];
      expect(data).toMatchInlineSnapshot(`
        [
          {
            "errors": [
              [
                "test-only: render error #1",
                1,
              ],
            ],
            "warnings": [],
          },
          {
            "errors": [
              [
                "test-only: render error #2",
                1,
              ],
            ],
            "warnings": [],
          },
        ]
      `);
    });

    it('can be cleared for a particular Fiber (only errors)', async () => {
      const Example = ({id}) => {
        console.error(`test-only: render error #${id}`);
        console.warn(`test-only: render warning #${id}`);
        return null;
      };

      const container = document.createElement('div');
      await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
        await utils.actAsync(() =>
          legacyRender(
            <React.Fragment>
              <Example id={1} />
              <Example id={2} />
            </React.Fragment>,
            container,
          ),
        );
      });

      let id = ((store.getElementIDAtIndex(1): any): number);
      const rendererID = store.getRendererIDForElement(id);

      const {
        clearErrorsForElement,
      } = require('react-devtools-shared/src/backendAPI');
      clearErrorsForElement({bridge, id, rendererID});

      // Flush events to the renderer.
      jest.runOnlyPendingTimers();

      let data = [
        await getErrorsAndWarningsForElementAtIndex(0),
        await getErrorsAndWarningsForElementAtIndex(1),
      ];
      expect(data).toMatchInlineSnapshot(`
        [
          {
            "errors": [
              [
                "test-only: render error #1",
                1,
              ],
            ],
            "warnings": [
              [
                "test-only: render warning #1",
                1,
              ],
            ],
          },
          {
            "errors": [],
            "warnings": [
              [
                "test-only: render warning #2",
                1,
              ],
            ],
          },
        ]
      `);

      id = ((store.getElementIDAtIndex(0): any): number);
      clearErrorsForElement({bridge, id, rendererID});

      // Flush events to the renderer.
      jest.runOnlyPendingTimers();

      data = [
        await getErrorsAndWarningsForElementAtIndex(0),
        await getErrorsAndWarningsForElementAtIndex(1),
      ];
      expect(data).toMatchInlineSnapshot(`
        [
          {
            "errors": [],
            "warnings": [
              [
                "test-only: render warning #1",
                1,
              ],
            ],
          },
          {
            "errors": [],
            "warnings": [
              [
                "test-only: render warning #2",
                1,
              ],
            ],
          },
        ]
      `);
    });
  });

  it('inspecting nested renderers should not throw', async () => {
    // Ignoring react art warnings
    jest.spyOn(console, 'error').mockImplementation(() => {});
    const ReactArt = require('react-art');
    const ArtSVGMode = require('art/modes/svg');
    const ARTCurrentMode = require('art/modes/current');
    store.componentFilters = [];

    ARTCurrentMode.setCurrent(ArtSVGMode);
    const {Surface, Group} = ReactArt;

    function Child() {
      return (
        <Surface width={1} height={1}>
          <Group />
        </Surface>
      );
    }
    function App() {
      return <Child />;
    }

    await utils.actAsync(() => {
      legacyRender(<App />, document.createElement('div'));
    });
    expect(store).toMatchInlineSnapshot(`
      [root]
        ▾ <App>
          ▾ <Child>
            ▾ <Surface>
                <svg>
      [root]
          <Group>
    `);

    const inspectedElement = await inspectElementAtIndex(4);
    expect(inspectedElement.owners).toMatchInlineSnapshot(`
      [
        {
          "compiledWithForget": false,
          "displayName": "Child",
          "hocDisplayNames": null,
          "id": 3,
          "key": null,
          "type": 5,
        },
        {
          "compiledWithForget": false,
          "displayName": "App",
          "hocDisplayNames": null,
          "id": 2,
          "key": null,
          "type": 5,
        },
      ]
    `);
  });

  describe('error boundary', () => {
    it('can toggle error', async () => {
      class LocalErrorBoundary extends React.Component<any> {
        state = {hasError: false};
        static getDerivedStateFromError(error) {
          return {hasError: true};
        }
        render() {
          const {hasError} = this.state;
          return hasError ? 'has-error' : this.props.children;
        }
      }

      const Example = () => 'example';

      await utils.actAsync(() =>
        legacyRender(
          <LocalErrorBoundary>
            <Example />
          </LocalErrorBoundary>,
          document.createElement('div'),
        ),
      );

      const targetErrorBoundaryID = ((store.getElementIDAtIndex(
        0,
      ): any): number);
      const inspect = index => {
        // HACK: Recreate TestRenderer instance so we can inspect different elements
        utils.withErrorsOrWarningsIgnored(
          ['An update to %s inside a test was not wrapped in act'],
          () => {
            testRendererInstance = TestRenderer.create(null, {
              isConcurrent: true,
            });
          },
        );
        return inspectElementAtIndex(index);
      };
      const toggleError = async forceError => {
        await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
          await TestUtilsAct(async () => {
            bridge.send('overrideError', {
              id: targetErrorBoundaryID,
              rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
              forceError,
            });
          });
        });

        await TestUtilsAct(async () => {
          jest.runOnlyPendingTimers();
        });
      };

      // Inspect <ErrorBoundary /> and see that we cannot toggle error state
      // on error boundary itself
      let inspectedElement = await inspect(0);
      expect(inspectedElement.canToggleError).toBe(false);
      expect(inspectedElement.targetErrorBoundaryID).toBe(null);

      // Inspect <Example />
      inspectedElement = await inspect(1);
      expect(inspectedElement.canToggleError).toBe(true);
      expect(inspectedElement.isErrored).toBe(false);
      expect(inspectedElement.targetErrorBoundaryID).toBe(
        targetErrorBoundaryID,
      );

      // Suppress expected error and warning.
      const consoleErrorMock = jest
        .spyOn(console, 'error')
        .mockImplementation(() => {});
      const consoleWarnMock = jest
        .spyOn(console, 'warn')
        .mockImplementation(() => {});

      // now force error state on <Example />
      await toggleError(true);

      consoleErrorMock.mockRestore();
      consoleWarnMock.mockRestore();

      // we are in error state now, <Example /> won't show up
      withErrorsOrWarningsIgnored(['Invalid index'], () => {
        expect(store.getElementIDAtIndex(1)).toBe(null);
      });

      // Inpsect <ErrorBoundary /> to toggle off the error state
      inspectedElement = await inspect(0);
      expect(inspectedElement.canToggleError).toBe(true);
      expect(inspectedElement.isErrored).toBe(true);
      // its error boundary ID is itself because it's caught the error
      expect(inspectedElement.targetErrorBoundaryID).toBe(
        targetErrorBoundaryID,
      );

      await toggleError(false);

      // We can now inspect <Example /> with ability to toggle again
      inspectedElement = await inspect(1);
      expect(inspectedElement.canToggleError).toBe(true);
      expect(inspectedElement.isErrored).toBe(false);
      expect(inspectedElement.targetErrorBoundaryID).toBe(
        targetErrorBoundaryID,
      );
    });
  });
});
