/** * Copyright (c) Facebook, Inc. and its 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 PropTypes; let TestRenderer: ReactTestRenderer; let bridge: FrontendBridge; let store: Store; let meta; let utils; let BridgeContext; let InspectedElementContext; let InspectedElementContextController; let StoreContext; let TestUtils; let TreeContextController; let TestUtilsAct; let TestRendererAct; beforeEach(() => { utils = require('./utils'); utils.beforeEachProfiling(); meta = require('react-devtools-shared/src/hydration').meta; bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; React = require('react'); ReactDOM = require('react-dom'); PropTypes = require('prop-types'); TestUtils = require('react-dom/test-utils'); TestUtilsAct = TestUtils.unstable_concurrentAct; TestRenderer = utils.requireTestRenderer(); TestRendererAct = TestUtils.unstable_concurrentAct; 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; StoreContext = require('react-devtools-shared/src/devtools/views/context') .StoreContext; TreeContextController = require('react-devtools-shared/src/devtools/views/Components/TreeContext') .TreeContextController; }); afterEach(() => { jest.restoreAllMocks(); }); const Contexts = ({ children, defaultSelectedElementID = null, defaultSelectedElementIndex = null, }) => ( {children} ); function useInspectedElement(id: number) { const {inspectedElement} = React.useContext(InspectedElementContext); return inspectedElement; } function useInspectElementPath(id: number) { const {inspectPaths} = React.useContext(InspectedElementContext); return inspectPaths; } it('should inspect the currently selected element', async done => { const Example = () => { const [count] = React.useState(1); return count; }; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let didFinish = false; function Suspender({target}) { const inspectedElement = useInspectedElement(id); expect(inspectedElement).toMatchInlineSnapshot(` Object { "context": null, "events": undefined, "hooks": Array [ Object { "id": 0, "isStateEditable": true, "name": "State", "subHooks": Array [], "value": 1, }, ], "id": 2, "owners": null, "props": Object { "a": 1, "b": "abc", }, "state": null, } `); didFinish = true; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(didFinish).toBe(true); done(); }); it('should have hasLegacyContext flag set to either "true" or "false" depending on which context API is used.', async done => { const contextData = { bool: true, }; // Legacy Context API. class LegacyContextProvider extends React.Component { static childContextTypes = { bool: PropTypes.bool, }; getChildContext() { return contextData; } render() { return this.props.children; } } class LegacyContextConsumer extends React.Component { 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 { static contextType = BoolContext; render() { return null; } } const ModernContext = React.createContext(); ModernContext.displayName = 'ModernContext'; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( {value => null} {value => null} , container, ), ); const ids = [ { // id: ((store.getElementIDAtIndex(1): any): number), shouldHaveLegacyContext: true, }, { // id: ((store.getElementIDAtIndex(2): any): number), shouldHaveLegacyContext: false, }, { // id: ((store.getElementIDAtIndex(3): any): number), shouldHaveLegacyContext: false, }, { // id: ((store.getElementIDAtIndex(5): any): number), shouldHaveLegacyContext: false, }, ]; function Suspender({target, shouldHaveLegacyContext}) { const inspectedElement = useInspectedElement(target); expect(inspectedElement.context).not.toBe(null); expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext); return null; } for (let i = 0; i < ids.length; i++) { const {id, shouldHaveLegacyContext} = ids[i]; await utils.actAsync( () => TestRenderer.create( , ), false, ); } done(); }); it('should poll for updates for the currently selected element', async done => { const Example = () => null; const container = document.createElement('div'); await utils.actAsync( () => ReactDOM.render(, container), false, ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } let renderer; await utils.actAsync(() => { renderer = TestRenderer.create( , ); }, false); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "a": 1, "b": "abc", } `); await utils.actAsync( () => ReactDOM.render(, 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(); await utils.actAsync( () => renderer.update( , ), false, ); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "a": 2, "b": "def", } `); done(); }); it('should not re-render a function with hooks if it did not update since it was last inspected', async done => { 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(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(1): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(target); return null; } targetRenderCount = 0; let renderer; await utils.actAsync( () => (renderer = TestRenderer.create( , )), false, ); expect(targetRenderCount).toBe(1); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "a": 1, "b": "abc", } `); const initialInspectedElement = inspectedElement; targetRenderCount = 0; inspectedElement = null; await utils.actAsync( () => renderer.update( , ), false, ); expect(targetRenderCount).toBe(0); expect(inspectedElement).toEqual(initialInspectedElement); targetRenderCount = 0; inspectedElement = null; await utils.actAsync( () => ReactDOM.render( , container, ), false, ); // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection. await utils.actAsync( () => renderer.update( , ), false, ); expect(targetRenderCount).toBe(2); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "a": 2, "b": "def", } `); done(); }); it('should temporarily disable console logging when re-running a component to inspect its hooks', async done => { 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'); await utils.actAsync(() => ReactDOM.render(, container), ); 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 id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(target); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); 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); done(); }); it('should support simple data types', async done => { const Example = () => null; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); const {props} = (inspectedElement: any); expect(props.boolean_false).toBe(false); expect(props.boolean_true).toBe(true); expect(Number.isFinite(props.infinity)).toBe(false); expect(props.integer_zero).toEqual(0); expect(props.integer_one).toEqual(1); expect(props.float).toEqual(1.23); expect(props.string).toEqual('abc'); expect(props.string_empty).toEqual(''); expect(props.nan).toBeNaN(); expect(props.value_null).toBeNull(); expect(props.value_undefined).toBeUndefined(); done(); }); it('should support complex data types', async done => { 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(() => ReactDOM.render( } regexp={/abc/giu} set={setShallow} set_of_sets={setOfSets} symbol={Symbol('symbol')} typed_array={typedArray} />, container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); const { anonymous_fn, array_buffer, array_of_arrays, big_int, bound_fn, data_view, date, fn, html_element, immutable, map, map_of_maps, object_of_objects, object_with_symbol, proxy, react_element, regexp, set, set_of_sets, symbol, typed_array, } = (inspectedElement: any).props; expect(anonymous_fn[meta.inspectable]).toBe(false); expect(anonymous_fn[meta.name]).toBe('function'); expect(anonymous_fn[meta.type]).toBe('function'); expect(anonymous_fn[meta.preview_long]).toBe('ƒ () {}'); expect(anonymous_fn[meta.preview_short]).toBe('ƒ () {}'); expect(array_buffer[meta.size]).toBe(3); expect(array_buffer[meta.inspectable]).toBe(false); expect(array_buffer[meta.name]).toBe('ArrayBuffer'); expect(array_buffer[meta.type]).toBe('array_buffer'); expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)'); expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)'); expect(array_of_arrays[0][meta.size]).toBe(2); expect(array_of_arrays[0][meta.inspectable]).toBe(true); expect(array_of_arrays[0][meta.name]).toBe('Array'); expect(array_of_arrays[0][meta.type]).toBe('array'); expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]'); expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)'); expect(big_int[meta.inspectable]).toBe(false); expect(big_int[meta.name]).toBe('123'); expect(big_int[meta.type]).toBe('bigint'); expect(big_int[meta.preview_long]).toBe('123n'); expect(big_int[meta.preview_short]).toBe('123n'); expect(bound_fn[meta.inspectable]).toBe(false); expect(bound_fn[meta.name]).toBe('bound exampleFunction'); expect(bound_fn[meta.type]).toBe('function'); expect(bound_fn[meta.preview_long]).toBe('ƒ bound exampleFunction() {}'); expect(bound_fn[meta.preview_short]).toBe('ƒ bound exampleFunction() {}'); expect(data_view[meta.size]).toBe(3); expect(data_view[meta.inspectable]).toBe(false); expect(data_view[meta.name]).toBe('DataView'); expect(data_view[meta.type]).toBe('data_view'); expect(data_view[meta.preview_long]).toBe('DataView(3)'); expect(data_view[meta.preview_short]).toBe('DataView(3)'); expect(date[meta.inspectable]).toBe(false); expect(date[meta.type]).toBe('date'); expect(new Date(date[meta.preview_long]).toISOString()).toBe( exampleDateISO, ); expect(new Date(date[meta.preview_short]).toISOString()).toBe( exampleDateISO, ); expect(fn[meta.inspectable]).toBe(false); expect(fn[meta.name]).toBe('exampleFunction'); expect(fn[meta.type]).toBe('function'); expect(fn[meta.preview_long]).toBe('ƒ exampleFunction() {}'); expect(fn[meta.preview_short]).toBe('ƒ exampleFunction() {}'); expect(html_element[meta.inspectable]).toBe(false); expect(html_element[meta.name]).toBe('DIV'); expect(html_element[meta.type]).toBe('html_element'); expect(html_element[meta.preview_long]).toBe('
'); expect(html_element[meta.preview_short]).toBe('
'); expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type expect(immutable[meta.name]).toBe('Map'); expect(immutable[meta.type]).toBe('iterator'); expect(immutable[meta.preview_long]).toBe( 'Map(3) {"a" => List(3), "b" => 123, "c" => Map(2)}', ); expect(immutable[meta.preview_short]).toBe('Map(3)'); expect(map[meta.inspectable]).toBeUndefined(); // Complex type expect(map[meta.name]).toBe('Map'); expect(map[meta.type]).toBe('iterator'); expect(map[0][meta.type]).toBe('array'); expect(map[meta.preview_long]).toBe( 'Map(2) {"name" => "Brian", "food" => "sushi"}', ); expect(map[meta.preview_short]).toBe('Map(2)'); expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type expect(map_of_maps[meta.name]).toBe('Map'); expect(map_of_maps[meta.type]).toBe('iterator'); expect(map_of_maps[0][meta.type]).toBe('array'); expect(map_of_maps[meta.preview_long]).toBe( 'Map(2) {"first" => Map(2), "second" => Map(2)}', ); expect(map_of_maps[meta.preview_short]).toBe('Map(2)'); expect(object_of_objects.inner[meta.size]).toBe(3); expect(object_of_objects.inner[meta.inspectable]).toBe(true); expect(object_of_objects.inner[meta.name]).toBe(''); expect(object_of_objects.inner[meta.type]).toBe('object'); expect(object_of_objects.inner[meta.preview_long]).toBe( '{boolean: true, number: 123, string: "abc"}', ); expect(object_of_objects.inner[meta.preview_short]).toBe('{…}'); expect(object_with_symbol['Symbol(name)']).toBe('hello'); expect(proxy[meta.inspectable]).toBe(false); expect(proxy[meta.name]).toBe('function'); expect(proxy[meta.type]).toBe('function'); expect(proxy[meta.preview_long]).toBe('ƒ () {}'); expect(proxy[meta.preview_short]).toBe('ƒ () {}'); expect(react_element[meta.inspectable]).toBe(false); expect(react_element[meta.name]).toBe('span'); expect(react_element[meta.type]).toBe('react_element'); expect(react_element[meta.preview_long]).toBe(''); expect(react_element[meta.preview_short]).toBe(''); expect(regexp[meta.inspectable]).toBe(false); expect(regexp[meta.name]).toBe('/abc/giu'); expect(regexp[meta.preview_long]).toBe('/abc/giu'); expect(regexp[meta.preview_short]).toBe('/abc/giu'); expect(regexp[meta.type]).toBe('regexp'); expect(set[meta.inspectable]).toBeUndefined(); // Complex type expect(set[meta.name]).toBe('Set'); expect(set[meta.type]).toBe('iterator'); expect(set[0]).toBe('abc'); expect(set[1]).toBe(123); expect(set[meta.preview_long]).toBe('Set(2) {"abc", 123}'); expect(set[meta.preview_short]).toBe('Set(2)'); expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type expect(set_of_sets[meta.name]).toBe('Set'); expect(set_of_sets[meta.type]).toBe('iterator'); expect(set_of_sets['0'][meta.inspectable]).toBe(true); expect(set_of_sets[meta.preview_long]).toBe('Set(2) {Set(3), Set(3)}'); expect(set_of_sets[meta.preview_short]).toBe('Set(2)'); expect(symbol[meta.inspectable]).toBe(false); expect(symbol[meta.name]).toBe('Symbol(symbol)'); expect(symbol[meta.type]).toBe('symbol'); expect(symbol[meta.preview_long]).toBe('Symbol(symbol)'); expect(symbol[meta.preview_short]).toBe('Symbol(symbol)'); expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type expect(typed_array[meta.size]).toBe(3); expect(typed_array[meta.name]).toBe('Int8Array'); expect(typed_array[meta.type]).toBe('typed_array'); expect(typed_array[0]).toBe(100); expect(typed_array[1]).toBe(-100); expect(typed_array[2]).toBe(0); expect(typed_array[meta.preview_long]).toBe('Int8Array(3) [100, -100, 0]'); expect(typed_array[meta.preview_short]).toBe('Int8Array(3)'); done(); }); it('should not consume iterables while inspecting', async done => { const Example = () => null; function* generator() { throw Error('Should not be consumed!'); } const container = document.createElement('div'); const iterable = generator(); await utils.actAsync(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); const {prop} = (inspectedElement: any).props; expect(prop[meta.inspectable]).toBe(false); expect(prop[meta.name]).toBe('Generator'); expect(prop[meta.type]).toBe('opaque_iterator'); expect(prop[meta.preview_long]).toBe('Generator'); expect(prop[meta.preview_short]).toBe('Generator'); done(); }); it('should support objects with no prototype', async done => { 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(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "object": Object { "boolean": true, "number": 123, "string": "abc", }, } `); done(); }); it('should support objects with overridden hasOwnProperty', async done => { const Example = () => null; const object = { name: 'blah', hasOwnProperty: true, }; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); // TRICKY: Don't use toMatchInlineSnapshot() for this test! // Our snapshot serializer relies on hasOwnProperty() for feature detection. expect(inspectedElement.props.object.name).toBe('blah'); expect(inspectedElement.props.object.hasOwnProperty).toBe(true); done(); }); it('should support custom objects with enumerable properties and getters', async done => { class CustomData { _number = 42; get number() { return this._number; } set number(value) { this._number = value; } } const descriptor = ((Object.getOwnPropertyDescriptor( CustomData.prototype, 'number', ): any): PropertyDescriptor); descriptor.enumerable = true; Object.defineProperty(CustomData.prototype, 'number', descriptor); const Example = () => null; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let didFinish = false; function Suspender({target}) { const inspectedElement = useInspectedElement(id); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "data": Object { "_number": 42, "number": 42, }, } `); didFinish = true; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(didFinish).toBe(true); done(); }); it('should support objects with with inherited keys', async done => { 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(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(id); return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "object": Object { "123": 3, "Symbol(enumerableSymbol)": 3, "Symbol(enumerableSymbolBase)": 1, "enumerableString": 2, "enumerableStringBase": 1, }, } `); done(); }); it('should not dehydrate nested values until explicitly requested', async done => { const Example = () => { const [state] = React.useState({ foo: { bar: { baz: 'hi', }, }, }); return state.foo.bar.baz; }; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; let inspectElementPath = null; function Suspender({path, target}) { inspectedElement = useInspectedElement(id); inspectElementPath = useInspectElementPath(id); return null; } const renderer = TestRenderer.create(null); async function getInspectedElement() { await utils.actAsync( () => renderer.update( , ), false, ); } // Render once to get a handle on inspectElementPath() await getInspectedElement(); async function loadPath(path) { TestUtilsAct(() => { TestRendererAct(() => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); }); await getInspectedElement(); } expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Dehydrated { "preview_short": {…}, "preview_long": {b: {…}}, }, }, } `); await loadPath(['props', 'nestedObject', 'a']); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "c": Dehydrated { "preview_short": Array(1), "preview_long": [{…}], }, }, }, }, } `); await loadPath(['props', 'nestedObject', 'a', 'b', 'c']); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "c": Array [ Object { "d": Dehydrated { "preview_short": {…}, "preview_long": {e: {…}}, }, }, ], }, }, }, } `); await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "c": Array [ Object { "d": Object { "e": Object {}, }, }, ], }, }, }, } `); await loadPath(['hooks', 0, 'value']); expect(inspectedElement.hooks).toMatchInlineSnapshot(` Array [ Object { "id": 0, "isStateEditable": true, "name": "State", "subHooks": Array [], "value": Object { "foo": Object { "bar": Dehydrated { "preview_short": {…}, "preview_long": {baz: "hi"}, }, }, }, }, ] `); await loadPath(['hooks', 0, 'value', 'foo', 'bar']); expect(inspectedElement.hooks).toMatchInlineSnapshot(` Array [ Object { "id": 0, "isStateEditable": true, "name": "State", "subHooks": Array [], "value": Object { "foo": Object { "bar": Object { "baz": "hi", }, }, }, }, ] `); done(); }); it('should dehydrate complex nested values when requested', async done => { const Example = () => null; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; let inspectElementPath = null; function Suspender({path, target}) { inspectedElement = useInspectedElement(id); inspectElementPath = useInspectElementPath(id); return null; } const renderer = TestRenderer.create(null); async function getInspectedElement() { await utils.actAsync( () => renderer.update( , ), false, ); } // Render once to get a handle on inspectElementPath() await getInspectedElement(); async function loadPath(path) { TestUtilsAct(() => { TestRendererAct(() => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); }); await getInspectedElement(); } expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "set_of_sets": Object { "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(` Object { "set_of_sets": Object { "0": Object { "0": 1, "1": 2, "2": 3, }, "1": Object { "0": "a", "1": "b", "2": "c", }, }, } `); done(); }); it('should include updates for nested values that were previously hydrated', async done => { const Example = () => null; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; let inspectElementPath = null; function Suspender({path, target}) { inspectedElement = useInspectedElement(id); inspectElementPath = useInspectElementPath(id); return null; } const renderer = TestRenderer.create(null); async function getInspectedElement() { await utils.actAsync( () => renderer.update( , ), false, ); } // Render once to get a handle on inspectElementPath() await getInspectedElement(); async function loadPath(path) { TestUtilsAct(() => { TestRendererAct(() => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); }); await getInspectedElement(); } expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "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(` Object { "nestedObject": Object { "a": Object { "b": Object { "value": 1, }, "value": 1, }, "c": Object { "d": Dehydrated { "preview_short": {…}, "preview_long": {e: {…}, value: 1}, }, "value": 1, }, }, } `); await loadPath(['props', 'nestedObject', 'c']); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "value": 1, }, "value": 1, }, "c": Object { "d": Object { "e": Dehydrated { "preview_short": {…}, "preview_long": {value: 1}, }, "value": 1, }, "value": 1, }, }, } `); TestRendererAct(() => { TestUtilsAct(() => { ReactDOM.render( , container, ); }); }); // Wait for pending poll-for-update and then update inspected element data. jest.runOnlyPendingTimers(); await Promise.resolve(); await getInspectedElement(); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "value": 2, }, "value": 2, }, "c": Object { "d": Object { "e": Dehydrated { "preview_short": {…}, "preview_long": {value: 2}, }, "value": 2, }, "value": 2, }, }, } `); done(); }); it('should not tear if hydration is requested after an update', async done => { const Example = () => null; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( , container, ), ); const id = ((store.getElementIDAtIndex(0): any): number); let inspectedElement = null; let inspectElementPath = null; function Suspender({path, target}) { inspectedElement = useInspectedElement(id); inspectElementPath = useInspectElementPath(id); return null; } const renderer = TestRenderer.create(null); async function getInspectedElement() { await utils.actAsync( () => renderer.update( , ), false, ); } // Render once to get a handle on inspectElementPath() await getInspectedElement(); async function loadPath(path) { TestUtilsAct(() => { TestRendererAct(() => { inspectElementPath(path); jest.runOnlyPendingTimers(); }); }); await getInspectedElement(); } expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Dehydrated { "preview_short": {…}, "preview_long": {b: {…}, value: 1}, }, "value": 1, }, } `); TestUtilsAct(() => { ReactDOM.render( , container, ); }); await loadPath(['props', 'nestedObject', 'a']); expect(inspectedElement.props).toMatchInlineSnapshot(` Object { "nestedObject": Object { "a": Object { "b": Object { "value": 2, }, "value": 2, }, "value": 2, }, } `); done(); }); it('should inspect hooks for components that only use context', async done => { const Context = React.createContext(true); const Example = () => { const value = React.useContext(Context); return value; }; const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render(, container), ); const id = ((store.getElementIDAtIndex(0): any): number); let didFinish = false; function Suspender({target}) { const inspectedElement = useInspectedElement(id); expect(inspectedElement).toMatchInlineSnapshot(` Object { "context": null, "events": undefined, "hooks": Array [ Object { "id": null, "isStateEditable": false, "name": "Context", "subHooks": Array [], "value": true, }, ], "id": 2, "owners": null, "props": Object { "a": 1, "b": "abc", }, "state": null, } `); didFinish = true; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(didFinish).toBe(true); done(); }); it('should enable inspected values to be stored as global variables', async done => { const Example = () => null; const nestedObject = { a: { value: 1, b: { value: 1, c: { value: 1, }, }, }, }; await utils.actAsync(() => ReactDOM.render( , document.createElement('div'), ), ); const id = ((store.getElementIDAtIndex(0): any): number); let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal); function Suspender({target}) { storeAsGlobal = (elementID: number, path: Array) => { const rendererID = store.getRendererIDForElement(elementID); if (rendererID !== null) { const { storeAsGlobal: storeAsGlobalAPI, } = require('react-devtools-shared/src/backendAPI'); storeAsGlobalAPI({ bridge, id: elementID, path, rendererID, }); } }; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); jest.spyOn(console, 'log').mockImplementation(() => {}); // Should store the whole value (not just the hydrated parts) storeAsGlobal(id, ['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(id, ['props', 'nestedObject', 'a', 'b']); jest.runOnlyPendingTimers(); expect(console.log).toHaveBeenCalledWith('$reactTemp1'); expect(global.$reactTemp1).toBe(nestedObject.a.b); done(); }); it('should enable inspected values to be copied to the clipboard', async done => { const Example = () => null; const nestedObject = { a: { value: 1, b: { value: 1, c: { value: 1, }, }, }, }; await utils.actAsync(() => ReactDOM.render( , document.createElement('div'), ), ); const id = ((store.getElementIDAtIndex(0): any): number); let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath); function Suspender({target}) { copyPath = (elementID: number, path: Array) => { const rendererID = store.getRendererIDForElement(elementID); if (rendererID !== null) { const { copyInspectedElementPath, } = require('react-devtools-shared/src/backendAPI'); copyInspectedElementPath({ bridge, id: elementID, path, rendererID, }); } }; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(copyPath).not.toBeNull(); // Should copy the whole value (not just the hydrated parts) copyPath(id, ['props', 'nestedObject']); jest.runOnlyPendingTimers(); expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1); expect(global.mockClipboardCopy).toHaveBeenCalledWith( JSON.stringify(nestedObject), ); global.mockClipboardCopy.mockReset(); // Should copy the nested property specified (not just the outer value) copyPath(id, ['props', 'nestedObject', 'a', 'b']); jest.runOnlyPendingTimers(); expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1); expect(global.mockClipboardCopy).toHaveBeenCalledWith( JSON.stringify(nestedObject.a.b), ); done(); }); it('should enable complex values to be copied to the clipboard', async done => { 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, }, }); // $FlowFixMe const bigInt = BigInt(123); // eslint-disable-line no-undef await utils.actAsync(() => ReactDOM.render( , document.createElement('div'), ), ); const id = ((store.getElementIDAtIndex(0): any): number); let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath); function Suspender({target}) { copyPath = (elementID: number, path: Array) => { const rendererID = store.getRendererIDForElement(elementID); if (rendererID !== null) { const { copyInspectedElementPath, } = require('react-devtools-shared/src/backendAPI'); copyInspectedElementPath({ bridge, id: elementID, path, rendererID, }); } }; return null; } await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(copyPath).not.toBeNull(); // Should copy the whole value (not just the hydrated parts) copyPath(id, ['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(id, ['props', 'bigInt']); jest.runOnlyPendingTimers(); expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1); expect(global.mockClipboardCopy).toHaveBeenCalledWith( JSON.stringify('123n'), ); global.mockClipboardCopy.mockReset(); // Should copy the nested property specified (not just the outer value) copyPath(id, ['props', 'typedArray']); jest.runOnlyPendingTimers(); expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1); expect(global.mockClipboardCopy).toHaveBeenCalledWith( JSON.stringify({0: 100, 1: -100, 2: 0}), ); done(); }); it('should display complex values of useDebugValue', async done => { let inspectedElement = null; function Suspender({target}) { inspectedElement = useInspectedElement(target); return null; } const container = document.createElement('div'); function useDebuggableHook() { React.useDebugValue({foo: 2}); React.useState(1); return 1; } function DisplayedComplexValue() { useDebuggableHook(); return null; } await utils.actAsync(() => ReactDOM.render(, container), ); const ignoredComplexValueIndex = 0; const ignoredComplexValueId = ((store.getElementIDAtIndex( ignoredComplexValueIndex, ): any): number); await utils.actAsync( () => TestRenderer.create( , ), false, ); expect(inspectedElement.hooks).toMatchInlineSnapshot(` Array [ Object { "id": null, "isStateEditable": false, "name": "DebuggableHook", "subHooks": Array [ Object { "id": 0, "isStateEditable": true, "name": "State", "subHooks": Array [], "value": 1, }, ], "value": Object { "foo": 2, }, }, ] `); done(); }); describe('inline errors and warnings', () => { // Some actions require the Fiber id. // In those instances you might want to make assertions based on the ID instead of the index. function getErrorsAndWarningsForElement(id: number) { const index = ((store.getIndexOfElementID(id): any): number); return getErrorsAndWarningsForElementAtIndex(index); } async function getErrorsAndWarningsForElementAtIndex(index) { const id = ((store.getElementIDAtIndex(index): any): number); let errors = null; let warnings = null; function Suspender({target}) { const inspectedElement = useInspectedElement(id); errors = inspectedElement.errors; warnings = inspectedElement.warnings; return null; } let root; await utils.actAsync(() => { root = TestRenderer.create( , ); }, 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(() => ReactDOM.render(, container), ); }); const data = await getErrorsAndWarningsForElementAtIndex(0); expect(data).toMatchInlineSnapshot(` Object { "errors": Array [ Array [ "test-only: render error", 1, ], ], "warnings": Array [ Array [ "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(() => ReactDOM.render(, container), ); }); const data = await getErrorsAndWarningsForElementAtIndex(0); expect(data).toMatchInlineSnapshot(` Object { "errors": Array [ Array [ "test-only: render error", 2, ], ], "warnings": Array [ Array [ "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(() => ReactDOM.render(, container), ); }); const data = await getErrorsAndWarningsForElementAtIndex(0); expect(data).toMatchInlineSnapshot(` Object { "errors": Array [ Array [ "test-only: useLayoutEffect error", 1, ], ], "warnings": Array [ Array [ "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(() => ReactDOM.render(, container), ); }); const data = await getErrorsAndWarningsForElementAtIndex(0); expect(data).toMatchInlineSnapshot(` Object { "errors": Array [ Array [ "test-only: useEffect error", 1, ], ], "warnings": Array [ Array [ "test-only: useEffect warning", 1, ], ], } `); }); it('from react get recorded without a component stack', async () => { const Example = () => { return [
]; }; const container = document.createElement('div'); await utils.withErrorsOrWarningsIgnored( ['Warning: Each child in a list should have a unique "key" prop.'], async () => { await utils.actAsync(() => ReactDOM.render(, container), ); }, ); const data = await getErrorsAndWarningsForElementAtIndex(0); expect(data).toMatchInlineSnapshot(` Object { "errors": Array [ Array [ "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": Array [], } `); }); 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(() => ReactDOM.render(, 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(` Object { "errors": Array [], "warnings": Array [], } `); }); 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(() => ReactDOM.render( , 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 getErrorsAndWarningsForElement(1), await getErrorsAndWarningsForElement(2), ]; expect(data).toMatchInlineSnapshot(` Array [ Object { "errors": Array [ Array [ "test-only: render error #1", 1, ], ], "warnings": Array [ Array [ "test-only: render warning #1", 1, ], ], }, Object { "errors": Array [ Array [ "test-only: render error #2", 1, ], ], "warnings": Array [], }, ] `); id = ((store.getElementIDAtIndex(0): any): number); clearWarningsForElement({bridge, id, rendererID}); // Flush events to the renderer. jest.runOnlyPendingTimers(); data = [ await getErrorsAndWarningsForElement(1), await getErrorsAndWarningsForElement(2), ]; expect(data).toMatchInlineSnapshot(` Array [ Object { "errors": Array [ Array [ "test-only: render error #1", 1, ], ], "warnings": Array [], }, Object { "errors": Array [ Array [ "test-only: render error #2", 1, ], ], "warnings": Array [], }, ] `); }); 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(() => ReactDOM.render( , 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 getErrorsAndWarningsForElement(1), await getErrorsAndWarningsForElement(2), ]; expect(data).toMatchInlineSnapshot(` Array [ Object { "errors": Array [ Array [ "test-only: render error #1", 1, ], ], "warnings": Array [ Array [ "test-only: render warning #1", 1, ], ], }, Object { "errors": Array [], "warnings": Array [ Array [ "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 getErrorsAndWarningsForElement(1), await getErrorsAndWarningsForElement(2), ]; expect(data).toMatchInlineSnapshot(` Array [ Object { "errors": Array [], "warnings": Array [ Array [ "test-only: render warning #1", 1, ], ], }, Object { "errors": Array [], "warnings": Array [ Array [ "test-only: render warning #2", 1, ], ], }, ] `); }); }); });