react/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js

2904 lines
77 KiB
JavaScript

/**
* 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('jest-react').act;
TestRenderer = utils.requireTestRenderer();
TestRendererAct = require('jest-react').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, {
unstable_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, {
unstable_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(() => {});
// Wait for our check-for-updates poll to get the new data.
jest.runOnlyPendingTimers();
await Promise.resolve();
// 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, {
unstable_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) {
TestUtilsAct(() => {
TestRendererAct(() => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
});
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) {
TestUtilsAct(() => {
TestRendererAct(() => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
});
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) {
TestUtilsAct(() => {
TestRendererAct(() => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
});
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,
},
},
}
`);
TestRendererAct(() => {
TestUtilsAct(() => {
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) {
TestUtilsAct(() => {
TestRendererAct(() => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
});
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},
},
},
}
`);
TestRendererAct(() => {
TestUtilsAct(() => {
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) {
TestUtilsAct(() => {
TestRendererAct(() => {
inspectElementPath(path);
jest.runOnlyPendingTimers();
});
});
inspectedElement = await inspectElementAtIndex(0);
}
expect(inspectedElement.props).toMatchInlineSnapshot(`
{
"nestedObject": {
"a": Dehydrated {
"preview_short": {…},
"preview_long": {b: {…}, value: 1},
},
"value": 1,
},
}
`);
TestUtilsAct(() => {
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),
);
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),
);
});
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,
},
});
// $FlowFixMe
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'),
);
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}),
);
});
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, {
unstable_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 () => {
spyOn(console, 'error');
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>,
{unstable_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
spyOn(console, 'error');
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(`
[
{
"displayName": "Child",
"hocDisplayNames": null,
"id": 3,
"key": null,
"type": 5,
},
{
"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, {
unstable_isConcurrent: true,
});
},
);
return inspectElementAtIndex(index);
};
const toggleError = async forceError => {
await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
await TestUtilsAct(() => {
bridge.send('overrideError', {
id: targetErrorBoundaryID,
rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
forceError,
});
});
});
TestUtilsAct(() => {
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,
);
});
});
});