mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
7a32d718b9
Alternative to #28295. Instead of stashing all of the Usables eagerly, we can extract them by replaying the render when we need them like we do with any other hook. We already had an implementation of `use()` but it wasn't quite complete. These can also include further DebugInfo on them such as what Server Component rendered the Promise or async debug info. This is nice just to see which use() calls were made in the side-panel but it can also be used to gather everything that might have suspended. Together with https://github.com/facebook/react/pull/28286 we cover the case when a Promise was used a child and if it was unwrapped with use(). Notably we don't cover a Promise that was thrown (although we do support that in a Server Component which maybe we shouldn't). Throwing a Promise isn't officially supported though and that use case should move to the use() Hook. The pattern of conditionally suspending based on cache also isn't really supported with the use() pattern. You should always call use() if you previously called use() with the same input. This also ensures that we can track what might have suspended rather than what actually did. One limitation of this strategy is that it's hard to find all the places something might suspend in a tree without rerendering all the fibers again. So we might need to still add something to the tree to indicate which Fibers may have further debug info / thenables.
3016 lines
80 KiB
JavaScript
3016 lines
80 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,
|
|
getLegacyRenderImplementation,
|
|
getModernRenderImplementation,
|
|
getVersionedRenderImplementation,
|
|
} 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 testRendererInstance;
|
|
|
|
let ErrorBoundary;
|
|
let errorBoundaryInstance;
|
|
|
|
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
beforeEach(() => {
|
|
utils = require('./utils');
|
|
utils.beforeEachProfiling();
|
|
|
|
bridge = global.bridge;
|
|
store = global.store;
|
|
store.collapseNodesByDefault = false;
|
|
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMClient = require('react-dom/client');
|
|
PropTypes = require('prop-types');
|
|
TestUtilsAct = require('internal-test-utils').act;
|
|
TestRenderer = utils.requireTestRenderer();
|
|
TestRendererAct = require('internal-test-utils').act;
|
|
|
|
BridgeContext =
|
|
require('react-devtools-shared/src/devtools/views/context').BridgeContext;
|
|
InspectedElementContext =
|
|
require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContext;
|
|
InspectedElementContextController =
|
|
require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext').InspectedElementContextController;
|
|
SettingsContextController =
|
|
require('react-devtools-shared/src/devtools/views/Settings/SettingsContext').SettingsContextController;
|
|
StoreContext =
|
|
require('react-devtools-shared/src/devtools/views/context').StoreContext;
|
|
TreeContextController =
|
|
require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController;
|
|
|
|
// Used by inspectElementAtIndex() helper function
|
|
utils.act(() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
});
|
|
|
|
errorBoundaryInstance = null;
|
|
|
|
ErrorBoundary = class extends React.Component {
|
|
state = {error: null};
|
|
componentDidCatch(error) {
|
|
this.setState({error});
|
|
}
|
|
render() {
|
|
errorBoundaryInstance = this;
|
|
|
|
if (this.state.error) {
|
|
return null;
|
|
}
|
|
return this.props.children;
|
|
}
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
const {render: legacyRender} = getLegacyRenderImplementation();
|
|
const {render: modernRender} = getModernRenderImplementation();
|
|
const {render} = getVersionedRenderImplementation();
|
|
|
|
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;
|
|
}
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// @reactVersion <= 18.2
|
|
xit('should inspect the currently selected element (legacy render)', async () => {
|
|
const Example = () => {
|
|
const [count] = React.useState(1);
|
|
return count;
|
|
};
|
|
|
|
await utils.actAsync(() => {
|
|
legacyRender(<Example a={1} b="abc" />);
|
|
});
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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 inspect the currently selected element (createRoot)', async () => {
|
|
const Example = () => {
|
|
const [count] = React.useState(1);
|
|
return count;
|
|
};
|
|
|
|
await utils.actAsync(() => {
|
|
modernRender(<Example a={1} b="abc" />);
|
|
});
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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": "createRoot()",
|
|
"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';
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<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>,
|
|
),
|
|
);
|
|
|
|
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.
|
|
withErrorsOrWarningsIgnored(
|
|
['An update to %s inside a test was not wrapped in act'],
|
|
() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(index);
|
|
|
|
expect(inspectedElement.context).not.toBe(null);
|
|
expect(inspectedElement.hasLegacyContext).toBe(shouldHaveLegacyContext);
|
|
}
|
|
});
|
|
|
|
it('should poll for updates for the currently selected element', async () => {
|
|
const Example = () => null;
|
|
|
|
await utils.actAsync(() => render(<Example a={1} b="abc" />), false);
|
|
|
|
let inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"a": 1,
|
|
"b": "abc",
|
|
}
|
|
`);
|
|
|
|
await utils.actAsync(() => render(<Example a={2} b="def" />), 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;
|
|
});
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Wrapper>
|
|
<Target a={1} b="abc" />
|
|
</Wrapper>,
|
|
),
|
|
);
|
|
|
|
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(
|
|
() =>
|
|
render(
|
|
<Wrapper>
|
|
<Target a={2} b="def" />
|
|
</Wrapper>,
|
|
),
|
|
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;
|
|
});
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Wrapper>
|
|
<Target a={1} b="abc" />
|
|
</Wrapper>,
|
|
),
|
|
);
|
|
|
|
targetRenderCount = 0;
|
|
|
|
let inspectedElement = await inspectElementAtIndex(1);
|
|
expect(targetRenderCount).toBe(1);
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"a": 1,
|
|
"b": "abc",
|
|
}
|
|
`);
|
|
|
|
const prevInspectedElement = inspectedElement;
|
|
|
|
// This test causes an intermediate error to be logged but we can ignore it.
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Clear the frontend cache to simulate DevTools being closed and re-opened.
|
|
// The backend still thinks the most recently-inspected element is still cached,
|
|
// so the frontend needs to tell it to resend a full value.
|
|
// We can verify this by asserting that the component is re-rendered again.
|
|
withErrorsOrWarningsIgnored(
|
|
['An update to %s inside a test was not wrapped in act'],
|
|
() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
const {
|
|
clearCacheForTests,
|
|
} = require('react-devtools-shared/src/inspectedElementMutableSource');
|
|
clearCacheForTests();
|
|
|
|
targetRenderCount = 0;
|
|
inspectedElement = await inspectElementAtIndex(1);
|
|
expect(targetRenderCount).toBe(1);
|
|
expect(inspectedElement).toEqual(prevInspectedElement);
|
|
});
|
|
|
|
it('should temporarily disable console logging when re-running a component to inspect its hooks', async () => {
|
|
let targetRenderCount = 0;
|
|
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
jest.spyOn(console, 'info').mockImplementation(() => {});
|
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const Target = React.memo(props => {
|
|
targetRenderCount++;
|
|
console.error('error');
|
|
console.info('info');
|
|
console.log('log');
|
|
console.warn('warn');
|
|
React.useState(0);
|
|
return null;
|
|
});
|
|
|
|
await utils.actAsync(() => 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;
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<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}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
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;
|
|
};
|
|
},
|
|
});
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<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}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
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 iterable = generator();
|
|
await utils.actAsync(() => render(<Example prop={iterable} />));
|
|
|
|
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;
|
|
|
|
await utils.actAsync(() => render(<Example object={object} />));
|
|
|
|
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,
|
|
};
|
|
|
|
await utils.actAsync(() => render(<Example object={object} />));
|
|
|
|
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;
|
|
|
|
await utils.actAsync(() => render(<Example data={new CustomData()} />));
|
|
|
|
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,
|
|
},
|
|
});
|
|
|
|
await utils.actAsync(() => render(<Example object={object} />));
|
|
|
|
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;
|
|
|
|
await utils.actAsync(() => render(<Example data={testData} />));
|
|
|
|
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;
|
|
};
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
a: {
|
|
b: {
|
|
c: [
|
|
{
|
|
d: {
|
|
e: {},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
let inspectedElement = null;
|
|
let inspectElementPath = null;
|
|
|
|
// Render once to get a handle on inspectElementPath()
|
|
inspectedElement = await inspectElementAtIndex(0, () => {
|
|
inspectElementPath = useInspectElementPath();
|
|
});
|
|
|
|
async function loadPath(path) {
|
|
await TestUtilsAct(async () => {
|
|
await TestRendererAct(async () => {
|
|
inspectElementPath(path);
|
|
});
|
|
});
|
|
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
}
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {b: {…}},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'a']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"c": Dehydrated {
|
|
"preview_short": Array(1),
|
|
"preview_long": [{…}],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'a', 'b', 'c']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"c": [
|
|
{
|
|
"d": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {e: {…}},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"c": [
|
|
{
|
|
"d": {
|
|
"e": {},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['hooks', 0, 'value']);
|
|
|
|
expect(inspectedElement.hooks).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"debugInfo": null,
|
|
"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(`
|
|
[
|
|
{
|
|
"debugInfo": null,
|
|
"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;
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
let inspectedElement = null;
|
|
let inspectElementPath = null;
|
|
|
|
// Render once to get a handle on inspectElementPath()
|
|
inspectedElement = await inspectElementAtIndex(0, () => {
|
|
inspectElementPath = useInspectElementPath();
|
|
});
|
|
|
|
async function loadPath(path) {
|
|
await TestUtilsAct(async () => {
|
|
await TestRendererAct(async () => {
|
|
inspectElementPath(path);
|
|
});
|
|
});
|
|
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
}
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"set_of_sets": {
|
|
"0": Dehydrated {
|
|
"preview_short": Set(3),
|
|
"preview_long": Set(3) {1, 2, 3},
|
|
},
|
|
"1": Dehydrated {
|
|
"preview_short": Set(3),
|
|
"preview_long": Set(3) {"a", "b", "c"},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'set_of_sets', 0]);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"set_of_sets": {
|
|
"0": {
|
|
"0": 1,
|
|
"1": 2,
|
|
"2": 3,
|
|
},
|
|
"1": Dehydrated {
|
|
"preview_short": Set(3),
|
|
"preview_long": Set(3) {"a", "b", "c"},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('should include updates for nested values that were previously hydrated', async () => {
|
|
const Example = () => null;
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
a: {
|
|
value: 1,
|
|
b: {
|
|
value: 1,
|
|
},
|
|
},
|
|
c: {
|
|
value: 1,
|
|
d: {
|
|
value: 1,
|
|
e: {
|
|
value: 1,
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
let inspectedElement = null;
|
|
let inspectElementPath = null;
|
|
|
|
// Render once to get a handle on inspectElementPath()
|
|
inspectedElement = await inspectElementAtIndex(0, () => {
|
|
inspectElementPath = useInspectElementPath();
|
|
});
|
|
|
|
async function loadPath(path) {
|
|
await TestUtilsAct(async () => {
|
|
await TestRendererAct(async () => {
|
|
inspectElementPath(path);
|
|
});
|
|
});
|
|
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
}
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {b: {…}, value: 1},
|
|
},
|
|
"c": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {d: {…}, value: 1},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'a']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"value": 1,
|
|
},
|
|
"value": 1,
|
|
},
|
|
"c": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {d: {…}, value: 1},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'c']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"value": 1,
|
|
},
|
|
"value": 1,
|
|
},
|
|
"c": {
|
|
"d": {
|
|
"e": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {value: 1},
|
|
},
|
|
"value": 1,
|
|
},
|
|
"value": 1,
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await TestRendererAct(async () => {
|
|
await TestUtilsAct(async () => {
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
a: {
|
|
value: 2,
|
|
b: {
|
|
value: 2,
|
|
},
|
|
},
|
|
c: {
|
|
value: 2,
|
|
d: {
|
|
value: 2,
|
|
e: {
|
|
value: 2,
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
a: {
|
|
value: 1,
|
|
b: {
|
|
value: 1,
|
|
},
|
|
},
|
|
c: {
|
|
value: 1,
|
|
d: {
|
|
value: 1,
|
|
e: {
|
|
value: 1,
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
let inspectedElement = null;
|
|
let inspectElementPath = null;
|
|
|
|
// Render once to get a handle on inspectElementPath()
|
|
inspectedElement = await inspectElementAtIndex(0, () => {
|
|
inspectElementPath = useInspectElementPath();
|
|
});
|
|
|
|
async function loadPath(path) {
|
|
await TestUtilsAct(async () => {
|
|
await TestRendererAct(() => {
|
|
inspectElementPath(path);
|
|
});
|
|
});
|
|
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
}
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {b: {…}, value: 1},
|
|
},
|
|
"c": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {d: {…}, value: 1},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await loadPath(['props', 'nestedObject', 'a']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"value": 1,
|
|
},
|
|
"value": 1,
|
|
},
|
|
"c": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {d: {…}, value: 1},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
|
|
await TestRendererAct(async () => {
|
|
await TestUtilsAct(async () => {
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
a: {
|
|
value: 2,
|
|
b: {
|
|
value: 2,
|
|
},
|
|
},
|
|
c: {
|
|
value: 2,
|
|
d: {
|
|
value: 2,
|
|
e: {
|
|
value: 2,
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
});
|
|
});
|
|
|
|
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;
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
value: 1,
|
|
a: {
|
|
value: 1,
|
|
b: {
|
|
value: 1,
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
let inspectedElement = null;
|
|
let inspectElementPath = null;
|
|
|
|
// Render once to get a handle on inspectElementPath()
|
|
inspectedElement = await inspectElementAtIndex(0, () => {
|
|
inspectElementPath = useInspectElementPath();
|
|
});
|
|
|
|
async function loadPath(path) {
|
|
await TestUtilsAct(async () => {
|
|
await TestRendererAct(() => {
|
|
inspectElementPath(path);
|
|
});
|
|
});
|
|
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
}
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": Dehydrated {
|
|
"preview_short": {…},
|
|
"preview_long": {b: {…}, value: 1},
|
|
},
|
|
"value": 1,
|
|
},
|
|
}
|
|
`);
|
|
|
|
await TestUtilsAct(async () => {
|
|
render(
|
|
<Example
|
|
nestedObject={{
|
|
value: 2,
|
|
a: {
|
|
value: 2,
|
|
b: {
|
|
value: 2,
|
|
},
|
|
},
|
|
}}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
await loadPath(['props', 'nestedObject', 'a']);
|
|
|
|
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
|
{
|
|
"nestedObject": {
|
|
"a": {
|
|
"b": {
|
|
"value": 2,
|
|
},
|
|
"value": 2,
|
|
},
|
|
"value": 2,
|
|
},
|
|
}
|
|
`);
|
|
});
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// @reactVersion <= 18.2
|
|
xit('should inspect hooks for components that only use context (legacy render)', async () => {
|
|
const Context = React.createContext(true);
|
|
const Example = () => {
|
|
const value = React.useContext(Context);
|
|
return value;
|
|
};
|
|
|
|
await utils.actAsync(() => legacyRender(<Example a={1} b="abc" />));
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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 inspect hooks for components that only use context (createRoot)', async () => {
|
|
const Context = React.createContext(true);
|
|
const Example = () => {
|
|
const value = React.useContext(Context);
|
|
return value;
|
|
};
|
|
|
|
await utils.actAsync(() => modernRender(<Example a={1} b="abc" />));
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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": "createRoot()",
|
|
"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(() => render(<Example nestedObject={nestedObject} />));
|
|
|
|
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(() => render(<Example nestedObject={nestedObject} />));
|
|
|
|
let copyPath: CopyInspectedElementPath =
|
|
((null: any): CopyInspectedElementPath);
|
|
|
|
const id = ((store.getElementIDAtIndex(0): any): number);
|
|
await inspectElementAtIndex(0, () => {
|
|
copyPath = (path: Array<string | number>) => {
|
|
const rendererID = store.getRendererIDForElement(id);
|
|
if (rendererID !== null) {
|
|
const {
|
|
copyInspectedElementPath,
|
|
} = require('react-devtools-shared/src/backendAPI');
|
|
copyInspectedElementPath({
|
|
bridge,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
// Should copy the whole value (not just the hydrated parts)
|
|
copyPath(['props', 'nestedObject']);
|
|
jest.runOnlyPendingTimers();
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
|
JSON.stringify(nestedObject, undefined, 2),
|
|
);
|
|
|
|
global.mockClipboardCopy.mockReset();
|
|
|
|
// Should copy the nested property specified (not just the outer value)
|
|
copyPath(['props', 'nestedObject', 'a', 'b']);
|
|
jest.runOnlyPendingTimers();
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
|
JSON.stringify(nestedObject.a.b, undefined, 2),
|
|
);
|
|
});
|
|
|
|
it('should enable complex values to be copied to the clipboard', async () => {
|
|
const Immutable = require('immutable');
|
|
|
|
const Example = () => null;
|
|
|
|
const set = new Set(['abc', 123]);
|
|
const map = new Map([
|
|
['name', 'Brian'],
|
|
['food', 'sushi'],
|
|
]);
|
|
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
|
|
const mapOfMaps = new Map([
|
|
['first', map],
|
|
['second', map],
|
|
]);
|
|
const typedArray = Int8Array.from([100, -100, 0]);
|
|
const arrayBuffer = typedArray.buffer;
|
|
const dataView = new DataView(arrayBuffer);
|
|
const immutable = Immutable.fromJS({
|
|
a: [{hello: 'there'}, 'fixed', true],
|
|
b: 123,
|
|
c: {
|
|
'1': 'xyz',
|
|
xyz: 1,
|
|
},
|
|
});
|
|
const bigInt = BigInt(123); // eslint-disable-line no-undef
|
|
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<Example
|
|
arrayBuffer={arrayBuffer}
|
|
dataView={dataView}
|
|
map={map}
|
|
set={set}
|
|
mapOfMaps={mapOfMaps}
|
|
setOfSets={setOfSets}
|
|
typedArray={typedArray}
|
|
immutable={immutable}
|
|
bigInt={bigInt}
|
|
/>,
|
|
),
|
|
);
|
|
|
|
const id = ((store.getElementIDAtIndex(0): any): number);
|
|
|
|
let copyPath: CopyInspectedElementPath =
|
|
((null: any): CopyInspectedElementPath);
|
|
|
|
await inspectElementAtIndex(0, () => {
|
|
copyPath = (path: Array<string | number>) => {
|
|
const rendererID = store.getRendererIDForElement(id);
|
|
if (rendererID !== null) {
|
|
const {
|
|
copyInspectedElementPath,
|
|
} = require('react-devtools-shared/src/backendAPI');
|
|
copyInspectedElementPath({
|
|
bridge,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
// Should copy the whole value (not just the hydrated parts)
|
|
copyPath(['props']);
|
|
jest.runOnlyPendingTimers();
|
|
// Should not error despite lots of unserialized values.
|
|
|
|
global.mockClipboardCopy.mockReset();
|
|
|
|
// Should copy the nested property specified (not just the outer value)
|
|
copyPath(['props', 'bigInt']);
|
|
jest.runOnlyPendingTimers();
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
|
JSON.stringify('123n', undefined, 2),
|
|
);
|
|
|
|
global.mockClipboardCopy.mockReset();
|
|
|
|
// Should copy the nested property specified (not just the outer value)
|
|
copyPath(['props', 'typedArray']);
|
|
jest.runOnlyPendingTimers();
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
|
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
|
|
);
|
|
});
|
|
|
|
it('should display complex values of useDebugValue', async () => {
|
|
function useDebuggableHook() {
|
|
React.useDebugValue({foo: 2});
|
|
React.useState(1);
|
|
return 1;
|
|
}
|
|
function DisplayedComplexValue() {
|
|
useDebuggableHook();
|
|
return null;
|
|
}
|
|
|
|
await utils.actAsync(() => render(<DisplayedComplexValue />));
|
|
|
|
const {hooks} = await inspectElementAtIndex(0);
|
|
expect(hooks).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"debugInfo": null,
|
|
"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": [
|
|
{
|
|
"debugInfo": null,
|
|
"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];
|
|
},
|
|
},
|
|
);
|
|
|
|
await utils.actAsync(() => render(<Example proxy={proxy} />));
|
|
|
|
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": ƒ () {},
|
|
},
|
|
},
|
|
}
|
|
`);
|
|
});
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// Regression test for github.com/facebook/react/issues/22099
|
|
// @reactVersion <= 18.2
|
|
xit('should not error when an unchanged component is re-inspected after component filters changed (legacy render)', async () => {
|
|
const Example = () => <div />;
|
|
|
|
await utils.actAsync(() => legacyRender(<Example />));
|
|
|
|
// 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
|
|
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.
|
|
withErrorsOrWarningsIgnored(
|
|
['An update to %s inside a test was not wrapped in act'],
|
|
() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
// Select/inspect the same element again
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": null,
|
|
"id": 2,
|
|
"owners": null,
|
|
"props": {},
|
|
"rootType": "render()",
|
|
"state": null,
|
|
}
|
|
`);
|
|
});
|
|
|
|
// Regression test for github.com/facebook/react/issues/22099
|
|
it('should not error when an unchanged component is re-inspected after component filters changed (createRoot)', async () => {
|
|
const Example = () => <div />;
|
|
|
|
await utils.actAsync(() => modernRender(<Example />));
|
|
|
|
// Select/inspect element
|
|
let inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": null,
|
|
"id": 2,
|
|
"owners": null,
|
|
"props": {},
|
|
"rootType": "createRoot()",
|
|
"state": null,
|
|
}
|
|
`);
|
|
|
|
await utils.actAsync(async () => {
|
|
// Ignore transient warning this causes
|
|
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.
|
|
withErrorsOrWarningsIgnored(
|
|
['An update to %s inside a test was not wrapped in act'],
|
|
() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
// Select/inspect the same element again
|
|
inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement).toMatchInlineSnapshot(`
|
|
{
|
|
"context": null,
|
|
"events": undefined,
|
|
"hooks": null,
|
|
"id": 2,
|
|
"owners": null,
|
|
"props": {},
|
|
"rootType": "createRoot()",
|
|
"state": null,
|
|
}
|
|
`);
|
|
});
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// @reactVersion <= 18.2
|
|
xit('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()"`);
|
|
});
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// @reactVersion <= 18.2
|
|
xit('should display the root type for ReactDOM.render', async () => {
|
|
const Example = () => <div />;
|
|
|
|
await utils.actAsync(() => {
|
|
legacyRender(<Example />);
|
|
}, false);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement.rootType).toMatchInlineSnapshot(`"render()"`);
|
|
});
|
|
|
|
it('should display the root type for ReactDOMClient.hydrateRoot', async () => {
|
|
const Example = () => <div />;
|
|
|
|
await utils.actAsync(() => {
|
|
const container = document.createElement('div');
|
|
container.innerHTML = '<div></div>';
|
|
ReactDOMClient.hydrateRoot(container, <Example />);
|
|
}, false);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement.rootType).toMatchInlineSnapshot(`"hydrateRoot()"`);
|
|
});
|
|
|
|
it('should display the root type for ReactDOMClient.createRoot', async () => {
|
|
const Example = () => <div />;
|
|
|
|
await utils.actAsync(() => {
|
|
const container = document.createElement('div');
|
|
ReactDOMClient.createRoot(container).render(<Example />);
|
|
}, false);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(0);
|
|
expect(inspectedElement.rootType).toMatchInlineSnapshot(`"createRoot()"`);
|
|
});
|
|
|
|
it('should gracefully surface backend errors on the frontend rather than timing out', async () => {
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
let shouldThrow = false;
|
|
|
|
const Example = () => {
|
|
const [count] = React.useState(0);
|
|
|
|
if (shouldThrow) {
|
|
throw Error('Expected');
|
|
} else {
|
|
return count;
|
|
}
|
|
};
|
|
|
|
await utils.actAsync(() => {
|
|
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;
|
|
};
|
|
|
|
await utils.actAsync(() => render(<Example a={1} b="abc" />));
|
|
|
|
await inspectElementAtIndex(0);
|
|
|
|
expect(global.$r).toMatchInlineSnapshot(`
|
|
{
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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;
|
|
});
|
|
|
|
await utils.actAsync(() => render(<Example a={1} b="abc" />));
|
|
|
|
await inspectElementAtIndex(0);
|
|
|
|
expect(global.$r).toMatchInlineSnapshot(`
|
|
{
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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;
|
|
});
|
|
|
|
await utils.actAsync(() => render(<Example a={1} b="abc" />));
|
|
|
|
await inspectElementAtIndex(0);
|
|
|
|
expect(global.$r).toMatchInlineSnapshot(`
|
|
{
|
|
"hooks": [
|
|
{
|
|
"debugInfo": null,
|
|
"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;
|
|
}
|
|
}
|
|
|
|
await utils.actAsync(() => render(<Example a={1} b="abc" />));
|
|
|
|
await inspectElementAtIndex(0);
|
|
|
|
expect(global.$r.props).toMatchInlineSnapshot(`
|
|
{
|
|
"a": 1,
|
|
"b": "abc",
|
|
}
|
|
`);
|
|
expect(global.$r.state).toMatchInlineSnapshot(`
|
|
{
|
|
"count": 0,
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('inline errors and warnings', () => {
|
|
async function getErrorsAndWarningsForElementAtIndex(index) {
|
|
const id = ((store.getElementIDAtIndex(index): any): number);
|
|
if (id == null) {
|
|
throw Error(`Element at index "${index}"" not found in store`);
|
|
}
|
|
|
|
let errors = null;
|
|
let warnings = null;
|
|
|
|
function Suspender({target}) {
|
|
const inspectedElement = useInspectedElement();
|
|
errors = inspectedElement.errors;
|
|
warnings = inspectedElement.warnings;
|
|
return null;
|
|
}
|
|
|
|
let root;
|
|
await utils.actAsync(() => {
|
|
root = TestRenderer.create(
|
|
<Contexts
|
|
defaultSelectedElementID={id}
|
|
defaultSelectedElementIndex={index}>
|
|
<React.Suspense fallback={null}>
|
|
<Suspender target={id} />
|
|
</React.Suspense>
|
|
</Contexts>,
|
|
{isConcurrent: true},
|
|
);
|
|
}, false);
|
|
await utils.actAsync(() => {
|
|
root.unmount();
|
|
}, false);
|
|
|
|
return {errors, warnings};
|
|
}
|
|
|
|
it('during render get recorded', async () => {
|
|
const Example = () => {
|
|
console.error('test-only: render error');
|
|
console.warn('test-only: render warning');
|
|
return null;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only: '], async () => {
|
|
await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
|
|
});
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
|
|
});
|
|
|
|
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 />];
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(
|
|
['Warning: Each child in a list should have a unique "key" prop.'],
|
|
async () => {
|
|
await utils.actAsync(() =>
|
|
render(<Example repeatWarningCount={1} />),
|
|
);
|
|
},
|
|
);
|
|
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() => render(<Example repeatWarningCount={1} />));
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<React.Fragment>
|
|
<Example id={1} />
|
|
<Example id={2} />
|
|
</React.Fragment>,
|
|
),
|
|
);
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
await withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
|
await utils.actAsync(() =>
|
|
render(
|
|
<React.Fragment>
|
|
<Example id={1} />
|
|
<Example id={2} />
|
|
</React.Fragment>,
|
|
),
|
|
);
|
|
});
|
|
|
|
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,
|
|
],
|
|
],
|
|
},
|
|
]
|
|
`);
|
|
});
|
|
});
|
|
|
|
// TODO(hoxyq): Enable this test for versions ~18, currently broken
|
|
// @reactVersion <= 18.2
|
|
xit('inspecting nested renderers should not throw (legacy render)', async () => {
|
|
// Ignoring react art warnings
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
const ReactArt = require('react-art');
|
|
const ArtSVGMode = require('art/modes/svg');
|
|
const ARTCurrentMode = require('art/modes/current');
|
|
store.componentFilters = [];
|
|
|
|
ARTCurrentMode.setCurrent(ArtSVGMode);
|
|
const {Surface, Group} = ReactArt;
|
|
|
|
function Child() {
|
|
return (
|
|
<Surface width={1} height={1}>
|
|
<Group />
|
|
</Surface>
|
|
);
|
|
}
|
|
function App() {
|
|
return <Child />;
|
|
}
|
|
|
|
await utils.actAsync(() => {
|
|
legacyRender(<App />);
|
|
});
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <App>
|
|
▾ <Child>
|
|
▾ <Surface>
|
|
<svg>
|
|
[root]
|
|
<Group>
|
|
`);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(4);
|
|
expect(inspectedElement.owners).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"compiledWithForget": false,
|
|
"displayName": "Child",
|
|
"hocDisplayNames": null,
|
|
"id": 3,
|
|
"key": null,
|
|
"type": 5,
|
|
},
|
|
{
|
|
"compiledWithForget": false,
|
|
"displayName": "App",
|
|
"hocDisplayNames": null,
|
|
"id": 2,
|
|
"key": null,
|
|
"type": 5,
|
|
},
|
|
]
|
|
`);
|
|
});
|
|
|
|
it('inspecting nested renderers should not throw (createRoot)', async () => {
|
|
// Ignoring react art warnings
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
const ReactArt = require('react-art');
|
|
const ArtSVGMode = require('art/modes/svg');
|
|
const ARTCurrentMode = require('art/modes/current');
|
|
store.componentFilters = [];
|
|
|
|
ARTCurrentMode.setCurrent(ArtSVGMode);
|
|
const {Surface, Group} = ReactArt;
|
|
|
|
function Child() {
|
|
return (
|
|
<Surface width={1} height={1}>
|
|
<Group />
|
|
</Surface>
|
|
);
|
|
}
|
|
function App() {
|
|
return <Child />;
|
|
}
|
|
|
|
await utils.actAsync(() => {
|
|
modernRender(<App />);
|
|
});
|
|
expect(store).toMatchInlineSnapshot(`
|
|
[root]
|
|
▾ <App>
|
|
▾ <Child>
|
|
▾ <Surface>
|
|
<svg>
|
|
[root]
|
|
<Group>
|
|
`);
|
|
|
|
const inspectedElement = await inspectElementAtIndex(4);
|
|
expect(inspectedElement.owners).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"compiledWithForget": false,
|
|
"displayName": "Child",
|
|
"hocDisplayNames": null,
|
|
"id": 5,
|
|
"key": null,
|
|
"type": 5,
|
|
},
|
|
{
|
|
"compiledWithForget": false,
|
|
"displayName": "App",
|
|
"hocDisplayNames": null,
|
|
"id": 4,
|
|
"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(() =>
|
|
render(
|
|
<LocalErrorBoundary>
|
|
<Example />
|
|
</LocalErrorBoundary>,
|
|
),
|
|
);
|
|
|
|
const targetErrorBoundaryID = ((store.getElementIDAtIndex(
|
|
0,
|
|
): any): number);
|
|
const inspect = index => {
|
|
// HACK: Recreate TestRenderer instance so we can inspect different elements
|
|
withErrorsOrWarningsIgnored(
|
|
['An update to %s inside a test was not wrapped in act'],
|
|
() => {
|
|
testRendererInstance = TestRenderer.create(null, {
|
|
isConcurrent: true,
|
|
});
|
|
},
|
|
);
|
|
return inspectElementAtIndex(index);
|
|
};
|
|
const toggleError = async forceError => {
|
|
await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
|
|
await TestUtilsAct(async () => {
|
|
bridge.send('overrideError', {
|
|
id: targetErrorBoundaryID,
|
|
rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
|
|
forceError,
|
|
});
|
|
});
|
|
});
|
|
|
|
await TestUtilsAct(async () => {
|
|
jest.runOnlyPendingTimers();
|
|
});
|
|
};
|
|
|
|
// Inspect <ErrorBoundary /> and see that we cannot toggle error state
|
|
// on error boundary itself
|
|
let inspectedElement = await inspect(0);
|
|
expect(inspectedElement.canToggleError).toBe(false);
|
|
expect(inspectedElement.targetErrorBoundaryID).toBe(null);
|
|
|
|
// Inspect <Example />
|
|
inspectedElement = await inspect(1);
|
|
expect(inspectedElement.canToggleError).toBe(true);
|
|
expect(inspectedElement.isErrored).toBe(false);
|
|
expect(inspectedElement.targetErrorBoundaryID).toBe(
|
|
targetErrorBoundaryID,
|
|
);
|
|
|
|
// Suppress expected error and warning.
|
|
const consoleErrorMock = jest
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
const consoleWarnMock = jest
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => {});
|
|
|
|
// now force error state on <Example />
|
|
await toggleError(true);
|
|
|
|
consoleErrorMock.mockRestore();
|
|
consoleWarnMock.mockRestore();
|
|
|
|
// we are in error state now, <Example /> won't show up
|
|
withErrorsOrWarningsIgnored(['Invalid index'], () => {
|
|
expect(store.getElementIDAtIndex(1)).toBe(null);
|
|
});
|
|
|
|
// Inpsect <ErrorBoundary /> to toggle off the error state
|
|
inspectedElement = await inspect(0);
|
|
expect(inspectedElement.canToggleError).toBe(true);
|
|
expect(inspectedElement.isErrored).toBe(true);
|
|
// its error boundary ID is itself because it's caught the error
|
|
expect(inspectedElement.targetErrorBoundaryID).toBe(
|
|
targetErrorBoundaryID,
|
|
);
|
|
|
|
await toggleError(false);
|
|
|
|
// We can now inspect <Example /> with ability to toggle again
|
|
inspectedElement = await inspect(1);
|
|
expect(inspectedElement.canToggleError).toBe(true);
|
|
expect(inspectedElement.isErrored).toBe(false);
|
|
expect(inspectedElement.targetErrorBoundaryID).toBe(
|
|
targetErrorBoundaryID,
|
|
);
|
|
});
|
|
});
|
|
});
|