mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #325 from bvaughn/hydration
Lazily send props/state/hooks across the bridge
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useDebugValue, useState } from 'react';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const exmapleFunction = () => {};
|
||||
const typedArray = new Uint8Array(3);
|
||||
typedArray[0] = 1;
|
||||
typedArray[1] = 2;
|
||||
typedArray[2] = 3;
|
||||
|
||||
const arrayOfArrays = [
|
||||
[['a', 'b', 'c'], ['d', 'e', 'f'], ['h', 'i', 'j']],
|
||||
[['k', 'l', 'm'], ['n', 'o', 'p'], ['q', 'r', 's']],
|
||||
[['t', 'u', 'v'], ['w', 'x', 'y'], ['z']],
|
||||
[],
|
||||
];
|
||||
|
||||
const objectOfObjects = {
|
||||
foo: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
},
|
||||
bar: {
|
||||
e: 4,
|
||||
f: 5,
|
||||
g: 6,
|
||||
},
|
||||
baz: {
|
||||
h: 7,
|
||||
i: 8,
|
||||
j: 9,
|
||||
},
|
||||
qux: {},
|
||||
};
|
||||
|
||||
function useOuterFoo() {
|
||||
useDebugValue({
|
||||
debugA: {
|
||||
debugB: {
|
||||
debugC: 'abc',
|
||||
},
|
||||
},
|
||||
});
|
||||
useState({
|
||||
valueA: {
|
||||
valueB: {
|
||||
valueC: 'abc',
|
||||
},
|
||||
},
|
||||
});
|
||||
return useInnerFoo();
|
||||
}
|
||||
|
||||
function useInnerFoo() {
|
||||
const [value] = useState([[['a', 'b', 'c']]]);
|
||||
return value;
|
||||
}
|
||||
|
||||
function useOuterBar() {
|
||||
useDebugValue({
|
||||
debugA: {
|
||||
debugB: {
|
||||
debugC: 'abc',
|
||||
},
|
||||
},
|
||||
});
|
||||
return useInnerBar();
|
||||
}
|
||||
|
||||
function useInnerBar() {
|
||||
useDebugValue({
|
||||
debugA: {
|
||||
debugB: {
|
||||
debugC: 'abc',
|
||||
},
|
||||
},
|
||||
});
|
||||
const [count] = useState(123);
|
||||
return count;
|
||||
}
|
||||
|
||||
function useOuterBaz() {
|
||||
return useInnerBaz();
|
||||
}
|
||||
|
||||
function useInnerBaz() {
|
||||
const [count] = useState(123);
|
||||
return count;
|
||||
}
|
||||
|
||||
export default function Hydration() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h1>Hydration</h1>
|
||||
<DehydratableProps
|
||||
html_element={div}
|
||||
fn={exmapleFunction}
|
||||
symbol={Symbol('symbol')}
|
||||
react_element={<span />}
|
||||
array_buffer={typedArray.buffer}
|
||||
typed_array={typedArray}
|
||||
date={new Date()}
|
||||
array={arrayOfArrays}
|
||||
object={objectOfObjects}
|
||||
/>
|
||||
<DeepHooks />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function DehydratableProps({ array, object }: any) {
|
||||
return (
|
||||
<ul>
|
||||
<li>array: {JSON.stringify(array, null, 2)}</li>
|
||||
<li>object: {JSON.stringify(object, null, 2)}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function DeepHooks(props: any) {
|
||||
const foo = useOuterFoo();
|
||||
const bar = useOuterBar();
|
||||
const baz = useOuterBaz();
|
||||
return (
|
||||
<ul>
|
||||
<li>foo: {foo}</li>
|
||||
<li>bar: {bar}</li>
|
||||
<li>baz: {baz}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import DeeplyNestedComponents from './DeeplyNestedComponents';
|
||||
import EditableProps from './EditableProps';
|
||||
import ElementTypes from './ElementTypes';
|
||||
import Hydration from './Hydration';
|
||||
import InspectableElements from './InspectableElements';
|
||||
import InteractionTracing from './InteractionTracing';
|
||||
import PriorityLevels from './PriorityLevels';
|
||||
@@ -36,6 +37,7 @@ function mountTestApp() {
|
||||
mountHelper(ToDoList);
|
||||
mountHelper(InteractionTracing);
|
||||
mountHelper(InspectableElements);
|
||||
mountHelper(Hydration);
|
||||
mountHelper(ElementTypes);
|
||||
mountHelper(EditableProps);
|
||||
mountHelper(PriorityLevels);
|
||||
|
||||
@@ -1,11 +1,101 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 1: mount 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {},
|
||||
"c": {}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 2: Inspected element 2 1`] = `
|
||||
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 2: Inspect props.nestedObject.a 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"value": 1,
|
||||
"b": {
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
"c": {}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 3: Inspect props.nestedObject.c 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"value": 1,
|
||||
"b": {
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
"c": {
|
||||
"value": 1,
|
||||
"d": {
|
||||
"value": 1,
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 4: update inspected element 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"value": 2,
|
||||
"b": {
|
||||
"value": 2
|
||||
}
|
||||
},
|
||||
"c": {
|
||||
"value": 2,
|
||||
"d": {
|
||||
"value": 2,
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
@@ -21,20 +111,218 @@ exports[`InspectedElementContext should inspect the currently selected element:
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
"a": 1,
|
||||
"b": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
<Anonymous>
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: initial render 1`] = `
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 5: Inspect hooks.0.value 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {
|
||||
"bar": {}
|
||||
}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 6: Inspect hooks.0.value.foo.bar 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": {
|
||||
"foo": {
|
||||
"bar": {
|
||||
"baz": "hi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: initial render 1`] = `
|
||||
{
|
||||
"id": 3,
|
||||
"owners": null,
|
||||
@@ -50,14 +338,14 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
"a": 1,
|
||||
"b": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 3: updated state 1`] = `
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: updated state 1`] = `
|
||||
{
|
||||
"id": 3,
|
||||
"owners": null,
|
||||
@@ -73,19 +361,14 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 2,
|
||||
"bar": "def"
|
||||
"a": 2,
|
||||
"b": "def"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 2: initial render 1`] = `
|
||||
exports[`InspectedElementContext should not tear if hydration is requested after an update: 1: Initially inspect element 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
@@ -93,8 +376,47 @@ exports[`InspectedElementContext should poll for updates for the currently selec
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
"nestedObject": {
|
||||
"value": 1,
|
||||
"a": {}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not tear if hydration is requested after an update: 2: Inspect props.nestedObject.a 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"value": 2,
|
||||
"a": {
|
||||
"value": 2,
|
||||
"b": {
|
||||
"value": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 1: initial render 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
@@ -108,19 +430,34 @@ exports[`InspectedElementContext should poll for updates for the currently selec
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"foo": 2,
|
||||
"bar": "def"
|
||||
"a": 2,
|
||||
"b": "def"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: mount 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
exports[`InspectedElementContext should support complex data types: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"html_element": {},
|
||||
"fn": {},
|
||||
"symbol": {},
|
||||
"react_element": {},
|
||||
"array_buffer": {},
|
||||
"typed_array": {},
|
||||
"date": {}
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 2: Inspected element 2 1`] = `
|
||||
exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type { Element } from 'src/devtools/views/Components/types';
|
||||
import type { GetInspectedElementPath } from 'src/devtools/views/Components/InspectedElementContext';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
@@ -11,24 +11,29 @@ describe('InspectedElementContext', () => {
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let meta;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let InspectedElementContext;
|
||||
let InspectedElementContextController;
|
||||
let StoreContext;
|
||||
let TestUtils;
|
||||
let TreeContextController;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
meta = require('src/hydration').meta;
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
@@ -68,20 +73,17 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example foo={1} bar="abc" />, container)
|
||||
ReactDOM.render(<Example a={1} b="abc" />, container)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const example = ((store.getElementAtIndex(0): any): Element);
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = read(target.id);
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
`2: Inspected element ${target.id}`
|
||||
);
|
||||
const { getInspectedElement } = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = getInspectedElement(id);
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
@@ -90,11 +92,11 @@ describe('InspectedElementContext', () => {
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
@@ -110,18 +112,17 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(
|
||||
() => ReactDOM.render(<Example foo={1} bar="abc" />, container),
|
||||
() => ReactDOM.render(<Example a={1} b="abc" />, container),
|
||||
false
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const example = ((store.getElementAtIndex(0): any): Element);
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = read(target.id);
|
||||
const { getInspectedElement } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,20 +130,17 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
await utils.actAsync(() => {
|
||||
renderer = TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
);
|
||||
}, false);
|
||||
expect(inspectedElement).toMatchSnapshot('2: initial render');
|
||||
expect(inspectedElement).toMatchSnapshot('1: initial render');
|
||||
|
||||
await utils.actAsync(
|
||||
() => ReactDOM.render(<Example foo={2} bar="def" />, container),
|
||||
() => ReactDOM.render(<Example a={2} b="def" />, container),
|
||||
false
|
||||
);
|
||||
|
||||
@@ -151,11 +149,11 @@ describe('InspectedElementContext', () => {
|
||||
() =>
|
||||
renderer.update(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
@@ -180,20 +178,19 @@ describe('InspectedElementContext', () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Wrapper>
|
||||
<Target foo={1} bar="abc" />
|
||||
<Target a={1} b="abc" />
|
||||
</Wrapper>,
|
||||
container
|
||||
)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const id = ((store.getElementIDAtIndex(1): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = read(target);
|
||||
const { getInspectedElement } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = getInspectedElement(target);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -215,7 +212,7 @@ describe('InspectedElementContext', () => {
|
||||
false
|
||||
);
|
||||
expect(targetRenderCount).toBe(1);
|
||||
expect(inspectedElement).toMatchSnapshot('2: initial render');
|
||||
expect(inspectedElement).toMatchSnapshot('1: initial render');
|
||||
|
||||
const initialInspectedElement = inspectedElement;
|
||||
|
||||
@@ -244,7 +241,7 @@ describe('InspectedElementContext', () => {
|
||||
() =>
|
||||
ReactDOM.render(
|
||||
<Wrapper>
|
||||
<Target foo={2} bar="def" />
|
||||
<Target a={2} b="def" />
|
||||
</Wrapper>,
|
||||
container
|
||||
),
|
||||
@@ -253,7 +250,93 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
// Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
|
||||
expect(targetRenderCount).toBe(2);
|
||||
expect(inspectedElement).toMatchSnapshot('3: updated state');
|
||||
expect(inspectedElement).toMatchSnapshot('2: updated state');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support complex data types', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const exmapleFunction = () => {};
|
||||
const typedArray = new Uint8Array(3);
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
html_element={div}
|
||||
fn={exmapleFunction}
|
||||
symbol={Symbol('symbol')}
|
||||
react_element={<span />}
|
||||
array_buffer={typedArray.buffer}
|
||||
typed_array={typedArray}
|
||||
date={new Date()}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { getInspectedElement } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
|
||||
const {
|
||||
html_element,
|
||||
fn,
|
||||
symbol,
|
||||
react_element,
|
||||
array_buffer,
|
||||
typed_array,
|
||||
date,
|
||||
} = (inspectedElement: any).props;
|
||||
expect(html_element[meta.inspectable]).toBe(false);
|
||||
expect(html_element[meta.name]).toBe('DIV');
|
||||
expect(html_element[meta.type]).toBe('html_element');
|
||||
expect(fn[meta.inspectable]).toBe(false);
|
||||
expect(fn[meta.name]).toBe('exmapleFunction');
|
||||
expect(fn[meta.type]).toBe('function');
|
||||
expect(symbol[meta.inspectable]).toBe(false);
|
||||
expect(symbol[meta.name]).toBe('Symbol(symbol)');
|
||||
expect(symbol[meta.type]).toBe('symbol');
|
||||
expect(react_element[meta.inspectable]).toBe(false);
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.inspectable]).toBe(false);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(typed_array[meta.size]).toBe(3);
|
||||
expect(typed_array[meta.inspectable]).toBe(false);
|
||||
expect(typed_array[meta.name]).toBe('Uint8Array');
|
||||
expect(typed_array[meta.type]).toBe('typed_array');
|
||||
expect(date[meta.inspectable]).toBe(false);
|
||||
expect(date[meta.type]).toBe('date');
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -276,24 +359,21 @@ describe('InspectedElementContext', () => {
|
||||
descriptor.enumerable = true;
|
||||
Object.defineProperty(CustomData.prototype, 'number', descriptor);
|
||||
|
||||
const Example = ({ data }) => null;
|
||||
const Example = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example data={new CustomData()} />, container)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const example = ((store.getElementAtIndex(0): any): Element);
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = read(target.id);
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
`2: Inspected element ${target.id}`
|
||||
);
|
||||
const { getInspectedElement } = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = getInspectedElement(id);
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
@@ -302,11 +382,11 @@ describe('InspectedElementContext', () => {
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
@@ -316,4 +396,315 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not dehydrate nested values until explicitly requested', async done => {
|
||||
const Example = () => {
|
||||
const [state] = React.useState({
|
||||
foo: {
|
||||
bar: {
|
||||
baz: 'hi',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return state.foo.bar.baz;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
b: {
|
||||
c: [
|
||||
{
|
||||
d: {
|
||||
e: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const context = React.useContext(InspectedElementContext);
|
||||
getInspectedElementPath = context.getInspectedElementPath;
|
||||
inspectedElement = context.getInspectedElement(target);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
false
|
||||
);
|
||||
expect(getInspectedElementPath).not.toBeNull();
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
'3: Inspect props.nestedObject.a.b.c'
|
||||
);
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, [
|
||||
'props',
|
||||
'nestedObject',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
0,
|
||||
'd',
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
'4: Inspect props.nestedObject.a.b.c.0.d'
|
||||
);
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
'6: Inspect hooks.0.value.foo.bar'
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should include updates for nested values that were previously hydrated', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
c: {
|
||||
value: 1,
|
||||
d: {
|
||||
value: 1,
|
||||
e: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const context = React.useContext(InspectedElementContext);
|
||||
getInspectedElementPath = context.getInspectedElementPath;
|
||||
inspectedElement = context.getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
false
|
||||
);
|
||||
expect(getInspectedElementPath).not.toBeNull();
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
|
||||
|
||||
TestUtils.act(() => {
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
value: 2,
|
||||
b: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
c: {
|
||||
value: 2,
|
||||
d: {
|
||||
value: 2,
|
||||
e: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
TestUtils.act(() => {
|
||||
inspectedElement = null;
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('4: update inspected element');
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not tear if hydration is requested after an update', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
value: 1,
|
||||
a: {
|
||||
value: 1,
|
||||
b: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const context = React.useContext(InspectedElementContext);
|
||||
getInspectedElementPath = context.getInspectedElementPath;
|
||||
inspectedElement = context.getInspectedElement(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
false
|
||||
);
|
||||
expect(getInspectedElementPath).not.toBeNull();
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
|
||||
|
||||
TestUtils.act(() => {
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
value: 2,
|
||||
a: {
|
||||
value: 2,
|
||||
b: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
});
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": "abc"
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"nestedObject": {
|
||||
"a": {
|
||||
"b": {
|
||||
"c": [
|
||||
{
|
||||
"d": {
|
||||
"e": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support complex data types: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"html_element": {},
|
||||
"fn": {},
|
||||
"symbol": {},
|
||||
"react_element": {},
|
||||
"array_buffer": {},
|
||||
"typed_array": {},
|
||||
"date": {}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Initial inspection 1`] = `
|
||||
Object {
|
||||
"id": 2,
|
||||
"type": "full-data",
|
||||
"value": {
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": {},
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"data": {
|
||||
"_number": 42,
|
||||
"number": 42
|
||||
}
|
||||
},
|
||||
"state": null
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,240 @@
|
||||
// @flow
|
||||
|
||||
import type { InspectedElementPayload } from 'src/backend/types';
|
||||
import type { DehydratedData } from 'src/devtools/views/Components/types';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('InspectedElementContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let hydrate;
|
||||
let meta;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
|
||||
const act = (callback: Function) => {
|
||||
callback();
|
||||
|
||||
jest.runAllTimers(); // Flush Bridge operations
|
||||
};
|
||||
|
||||
function dehydrateHelper(
|
||||
dehydratedData: DehydratedData | null
|
||||
): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function read(
|
||||
id: number,
|
||||
path?: Array<string | number>
|
||||
): Promise<Object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
||||
|
||||
const onInspectedElement = (payload: InspectedElementPayload) => {
|
||||
bridge.removeListener('inspectedElement', onInspectedElement);
|
||||
|
||||
if (payload.type === 'full-data' && payload.value !== null) {
|
||||
payload.value.context = dehydrateHelper(payload.value.context);
|
||||
payload.value.props = dehydrateHelper(payload.value.props);
|
||||
payload.value.state = dehydrateHelper(payload.value.state);
|
||||
}
|
||||
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
bridge.send('inspectElement', { id, path, rendererID });
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
|
||||
hydrate = require('src/hydration').hydrate;
|
||||
meta = require('src/hydration').meta;
|
||||
|
||||
// Redirect all React/ReactDOM requires to the v15 UMD.
|
||||
// We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
|
||||
jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
|
||||
jest.mock('react-dom', () =>
|
||||
jest.requireActual('react-dom-15/dist/react-dom.js')
|
||||
);
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
});
|
||||
|
||||
it('should inspect the currently selected element', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(<Example a={1} b="abc" />, document.createElement('div'))
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const inspectedElement = await read(id);
|
||||
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support complex data types', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const exmapleFunction = () => {};
|
||||
const typedArray = new Uint8Array(3);
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
html_element={div}
|
||||
fn={exmapleFunction}
|
||||
symbol={Symbol('symbol')}
|
||||
react_element={<span />}
|
||||
array_buffer={typedArray.buffer}
|
||||
typed_array={typedArray}
|
||||
date={new Date()}
|
||||
/>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const inspectedElement = await read(id);
|
||||
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
|
||||
const {
|
||||
html_element,
|
||||
fn,
|
||||
symbol,
|
||||
react_element,
|
||||
array_buffer,
|
||||
typed_array,
|
||||
date,
|
||||
} = inspectedElement.value.props;
|
||||
expect(html_element[meta.inspectable]).toBe(false);
|
||||
expect(html_element[meta.name]).toBe('DIV');
|
||||
expect(html_element[meta.type]).toBe('html_element');
|
||||
expect(fn[meta.inspectable]).toBe(false);
|
||||
expect(fn[meta.name]).toBe('exmapleFunction');
|
||||
expect(fn[meta.type]).toBe('function');
|
||||
expect(symbol[meta.inspectable]).toBe(false);
|
||||
expect(symbol[meta.name]).toBe('Symbol(symbol)');
|
||||
expect(symbol[meta.type]).toBe('symbol');
|
||||
expect(react_element[meta.inspectable]).toBe(false);
|
||||
expect(react_element[meta.name]).toBe('span');
|
||||
expect(react_element[meta.type]).toBe('react_element');
|
||||
expect(array_buffer[meta.size]).toBe(3);
|
||||
expect(array_buffer[meta.inspectable]).toBe(false);
|
||||
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
|
||||
expect(array_buffer[meta.type]).toBe('array_buffer');
|
||||
expect(typed_array[meta.size]).toBe(3);
|
||||
expect(typed_array[meta.inspectable]).toBe(false);
|
||||
expect(typed_array[meta.name]).toBe('Uint8Array');
|
||||
expect(typed_array[meta.type]).toBe('typed_array');
|
||||
expect(date[meta.inspectable]).toBe(false);
|
||||
expect(date[meta.type]).toBe('date');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should support custom objects with enumerable properties and getters', async done => {
|
||||
class CustomData {
|
||||
_number = 42;
|
||||
get number() {
|
||||
return this._number;
|
||||
}
|
||||
set number(value) {
|
||||
this._number = value;
|
||||
}
|
||||
}
|
||||
|
||||
const descriptor = ((Object.getOwnPropertyDescriptor(
|
||||
CustomData.prototype,
|
||||
'number'
|
||||
): any): PropertyDescriptor<number>);
|
||||
descriptor.enumerable = true;
|
||||
Object.defineProperty(CustomData.prototype, 'number', descriptor);
|
||||
|
||||
const Example = ({ data }) => null;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example data={new CustomData()} />,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
const inspectedElement = await read(id);
|
||||
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not dehydrate nested values until explicitly requested', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
b: {
|
||||
c: [
|
||||
{
|
||||
d: {
|
||||
e: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let inspectedElement = await read(id);
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
|
||||
|
||||
inspectedElement = await read(id, ['props', 'nestedObject', 'a']);
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
inspectedElement = await read(id, ['props', 'nestedObject', 'a', 'b', 'c']);
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
'3: Inspect props.nestedObject.a.b.c'
|
||||
);
|
||||
|
||||
inspectedElement = await read(id, [
|
||||
'props',
|
||||
'nestedObject',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
0,
|
||||
'd',
|
||||
]);
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
'4: Inspect props.nestedObject.a.b.c.0.d'
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,12 @@ type ElementAndRendererID = {|
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type InspectElementParams = {|
|
||||
id: number,
|
||||
path?: Array<string | number>,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type OverrideHookParams = {|
|
||||
id: number,
|
||||
hookID: number,
|
||||
@@ -242,12 +248,12 @@ export default class Agent extends EventEmitter<{|
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
inspectElement = ({ id, path, rendererID }: InspectElementParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
this._bridge.send('inspectedElement', renderer.inspectElement(id));
|
||||
this._bridge.send('inspectedElement', renderer.inspectElement(id, path));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { decorateMany, forceUpdate, restoreMany } from './utils';
|
||||
import type {
|
||||
DevToolsHook,
|
||||
GetFiberIDForNative,
|
||||
InspectedElementPayload,
|
||||
NativeType,
|
||||
PathFrame,
|
||||
PathMatch,
|
||||
@@ -548,16 +549,78 @@ export function attach(
|
||||
return stringID;
|
||||
}
|
||||
|
||||
function inspectElement(id: number): InspectedElement | null {
|
||||
let result = inspectElementRaw(id);
|
||||
if (result === null) {
|
||||
return null;
|
||||
let currentlyInspectedElementID: number | null = null;
|
||||
let currentlyInspectedPaths: Object = {};
|
||||
|
||||
// Track the intersection of currently inspected paths,
|
||||
// so that we can send their data along if the element is re-rendered.
|
||||
function mergeInspectedPaths(path: Array<string | number>) {
|
||||
let current = currentlyInspectedPaths;
|
||||
path.forEach(key => {
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
});
|
||||
}
|
||||
|
||||
function createIsPathWhitelisted(key: string) {
|
||||
// This function helps prevent previously-inspected paths from being dehydrated in updates.
|
||||
// This is important to avoid a bad user experience where expanded toggles collapse on update.
|
||||
return function isPathWhitelisted(path: Array<string | number>): boolean {
|
||||
let current = currentlyInspectedPaths[key];
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
current = current[path[i]];
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function inspectElement(
|
||||
id: number,
|
||||
path?: Array<string | number>
|
||||
): InspectedElementPayload {
|
||||
if (currentlyInspectedElementID !== id) {
|
||||
currentlyInspectedElementID = id;
|
||||
currentlyInspectedPaths = {};
|
||||
}
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
result.context = cleanForBridge(result.context);
|
||||
result.props = cleanForBridge(result.props);
|
||||
result.state = cleanForBridge(result.state);
|
||||
return result;
|
||||
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement === null) {
|
||||
return {
|
||||
id,
|
||||
type: 'not-found',
|
||||
};
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
mergeInspectedPaths(path);
|
||||
}
|
||||
|
||||
inspectedElement.context = cleanForBridge(
|
||||
inspectedElement.context,
|
||||
createIsPathWhitelisted('context')
|
||||
);
|
||||
inspectedElement.props = cleanForBridge(
|
||||
inspectedElement.props,
|
||||
createIsPathWhitelisted('props')
|
||||
);
|
||||
inspectedElement.state = cleanForBridge(
|
||||
inspectedElement.state,
|
||||
createIsPathWhitelisted('state')
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'full-data',
|
||||
value: inspectedElement,
|
||||
};
|
||||
}
|
||||
|
||||
function inspectElementRaw(id: number): InspectedElement | null {
|
||||
|
||||
+152
-28
@@ -22,11 +22,13 @@ import {
|
||||
import {
|
||||
getDisplayName,
|
||||
getDefaultComponentFilters,
|
||||
getInObject,
|
||||
getUID,
|
||||
setInObject,
|
||||
utfEncodeString,
|
||||
} from 'src/utils';
|
||||
import { sessionStorageGetItem } from 'src/storage';
|
||||
import { cleanForBridge, copyWithSet, setInObject } from './utils';
|
||||
import { cleanForBridge, copyWithSet } from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
@@ -44,6 +46,7 @@ import type {
|
||||
DevToolsHook,
|
||||
Fiber,
|
||||
InspectedElement,
|
||||
InspectedElementPayload,
|
||||
Owner,
|
||||
PathFrame,
|
||||
PathMatch,
|
||||
@@ -1297,8 +1300,8 @@ export function attach(
|
||||
}
|
||||
|
||||
if (
|
||||
mostRecentlyInspectedElementID !== null &&
|
||||
mostRecentlyInspectedElementID ===
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id ===
|
||||
getFiberID(getPrimaryFiber(nextFiber)) &&
|
||||
didFiberRender(prevFiber, nextFiber)
|
||||
) {
|
||||
@@ -2122,37 +2125,158 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
let mostRecentlyInspectedElementID: number | null = null;
|
||||
let mostRecentlyInspectedElement: InspectedElement | null = null;
|
||||
let hasElementUpdatedSinceLastInspected: boolean = false;
|
||||
let currentlyInspectedPaths: Object = {};
|
||||
|
||||
function inspectElement(id: number): InspectedElement | number | null {
|
||||
// If this element has not been updated since it was last inspected, we don't need to re-run it.
|
||||
// Instead we can just return the ID to indicate that it has not changed.
|
||||
if (
|
||||
mostRecentlyInspectedElementID === id &&
|
||||
function isMostRecentlyInspectedElementCurrent(id: number): boolean {
|
||||
return (
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id === id &&
|
||||
!hasElementUpdatedSinceLastInspected
|
||||
) {
|
||||
return id;
|
||||
);
|
||||
}
|
||||
|
||||
// Track the intersection of currently inspected paths,
|
||||
// so that we can send their data along if the element is re-rendered.
|
||||
function mergeInspectedPaths(path: Array<string | number>) {
|
||||
let current = currentlyInspectedPaths;
|
||||
path.forEach(key => {
|
||||
if (!current[key]) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
});
|
||||
}
|
||||
|
||||
function createIsPathWhitelisted(isHooksPath: boolean, key: string | null) {
|
||||
// This function helps prevent previously-inspected paths from being dehydrated in updates.
|
||||
// This is important to avoid a bad user experience where expanded toggles collapse on update.
|
||||
return function isPathWhitelisted(path: Array<string | number>): boolean {
|
||||
// Dehydrating the 'subHooks' property makes the HooksTree UI a lot more complicated,
|
||||
// so it's easiest for now if we just don't break on this boundary.
|
||||
// We can always dehydrate a level deeper (in the value object).
|
||||
if (isHooksPath) {
|
||||
if (path.length === 1) {
|
||||
// Never dehydrate the hooks object at the top level.
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
path[path.length - 1] === 'subHooks' ||
|
||||
path[path.length - 2] === 'subHooks'
|
||||
) {
|
||||
// Never dehydrate the subHooks array
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let current =
|
||||
key === null ? currentlyInspectedPaths : currentlyInspectedPaths[key];
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
current = current[path[i]];
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function inspectElement(
|
||||
id: number,
|
||||
path?: Array<string | number>
|
||||
): InspectedElementPayload {
|
||||
const isCurrent = isMostRecentlyInspectedElementCurrent(id);
|
||||
|
||||
if (isCurrent) {
|
||||
if (path != null) {
|
||||
mergeInspectedPaths(path);
|
||||
|
||||
// If this element has not been updated since it was last inspected,
|
||||
// we can just return the subset of data in the newly-inspected path.
|
||||
return {
|
||||
id,
|
||||
type: 'hydrated-path',
|
||||
path,
|
||||
value: cleanForBridge(
|
||||
getInObject(
|
||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||
path
|
||||
),
|
||||
createIsPathWhitelisted(path[0] === 'hooks', null),
|
||||
path
|
||||
),
|
||||
};
|
||||
} else {
|
||||
// If this element has not been updated since it was last inspected, we don't need to re-run it.
|
||||
// Instead we can just return the ID to indicate that it has not changed.
|
||||
return {
|
||||
id,
|
||||
type: 'no-change',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
hasElementUpdatedSinceLastInspected = false;
|
||||
|
||||
if (
|
||||
mostRecentlyInspectedElement === null ||
|
||||
mostRecentlyInspectedElement.id !== id
|
||||
) {
|
||||
currentlyInspectedPaths = {};
|
||||
}
|
||||
|
||||
mostRecentlyInspectedElement = inspectElementRaw(id);
|
||||
if (mostRecentlyInspectedElement === null) {
|
||||
return {
|
||||
id,
|
||||
type: 'not-found',
|
||||
};
|
||||
}
|
||||
|
||||
if (path != null) {
|
||||
mergeInspectedPaths(path);
|
||||
}
|
||||
|
||||
// Clone before cleaning so that we preserve the full data.
|
||||
// This will enable us to send patches without re-inspecting if hydrated paths are requested.
|
||||
// (Reducing how often we shallow-render is a better DX for function components that use hooks.)
|
||||
const cleanedInspectedElement = { ...mostRecentlyInspectedElement };
|
||||
cleanedInspectedElement.context = cleanForBridge(
|
||||
cleanedInspectedElement.context,
|
||||
createIsPathWhitelisted(false, 'context')
|
||||
);
|
||||
cleanedInspectedElement.events = cleanForBridge(
|
||||
cleanedInspectedElement.events,
|
||||
createIsPathWhitelisted(false, 'events')
|
||||
);
|
||||
cleanedInspectedElement.hooks = cleanForBridge(
|
||||
cleanedInspectedElement.hooks,
|
||||
createIsPathWhitelisted(true, 'hooks')
|
||||
);
|
||||
cleanedInspectedElement.props = cleanForBridge(
|
||||
cleanedInspectedElement.props,
|
||||
createIsPathWhitelisted(false, 'props')
|
||||
);
|
||||
cleanedInspectedElement.state = cleanForBridge(
|
||||
cleanedInspectedElement.state,
|
||||
createIsPathWhitelisted(false, 'state')
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'full-data',
|
||||
value: cleanedInspectedElement,
|
||||
};
|
||||
}
|
||||
|
||||
mostRecentlyInspectedElementID = id;
|
||||
hasElementUpdatedSinceLastInspected = false;
|
||||
|
||||
const inspectedElement = inspectElementRaw(id);
|
||||
if (inspectedElement === null) {
|
||||
return null;
|
||||
}
|
||||
inspectedElement.context = cleanForBridge(inspectedElement.context);
|
||||
inspectedElement.events = cleanForBridge(inspectedElement.events);
|
||||
inspectedElement.hooks = cleanForBridge(inspectedElement.hooks);
|
||||
inspectedElement.props = cleanForBridge(inspectedElement.props);
|
||||
inspectedElement.state = cleanForBridge(inspectedElement.state);
|
||||
|
||||
return inspectedElement;
|
||||
}
|
||||
|
||||
function logElementToConsole(id) {
|
||||
const result = inspectElementRaw(id);
|
||||
const result = isMostRecentlyInspectedElementCurrent(id)
|
||||
? mostRecentlyInspectedElement
|
||||
: inspectElementRaw(id);
|
||||
if (result === null) {
|
||||
console.warn(`Could not find Fiber with id "${id}"`);
|
||||
return;
|
||||
@@ -2690,10 +2814,10 @@ export function attach(
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
findNativeNodesForFiberID,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
getFiberIDForNative,
|
||||
findNativeNodesForFiberID,
|
||||
getOwnersList,
|
||||
getPathForElement,
|
||||
getProfilingData,
|
||||
|
||||
+38
-1
@@ -215,6 +215,40 @@ export type InspectedElement = {|
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
export const InspectElementFullDataType = 'full-data';
|
||||
export const InspectElementNoChangeType = 'no-change';
|
||||
export const InspectElementNotFoundType = 'not-found';
|
||||
export const InspectElementHydratedPathType = 'hydrated-path';
|
||||
|
||||
type InspectElementFullData = {|
|
||||
id: number,
|
||||
type: 'full-data',
|
||||
value: InspectedElement,
|
||||
|};
|
||||
|
||||
type InspectElementHydratedPath = {|
|
||||
id: number,
|
||||
type: 'hydrated-path',
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
|};
|
||||
|
||||
type InspectElementNoChange = {|
|
||||
id: number,
|
||||
type: 'no-change',
|
||||
|};
|
||||
|
||||
type InspectElementNotFound = {|
|
||||
id: number,
|
||||
type: 'not-found',
|
||||
|};
|
||||
|
||||
export type InspectedElementPayload =
|
||||
| InspectElementFullData
|
||||
| InspectElementHydratedPath
|
||||
| InspectElementNoChange
|
||||
| InspectElementNotFound;
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
findNativeNodesForFiberID: FindNativeNodesForFiberID,
|
||||
@@ -226,7 +260,10 @@ export type RendererInterface = {
|
||||
getPathForElement: (id: number) => Array<PathFrame> | null,
|
||||
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
|
||||
handleCommitFiberUnmount: (fiber: Object) => void,
|
||||
inspectElement: (id: number) => InspectedElement | number | null,
|
||||
inspectElement: (
|
||||
id: number,
|
||||
path?: Array<string | number>
|
||||
) => InspectedElementPayload,
|
||||
logElementToConsole: (id: number) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
prepareViewElementSource: (id: number) => void,
|
||||
|
||||
+7
-20
@@ -4,12 +4,16 @@ import { dehydrate } from '../hydration';
|
||||
|
||||
import type { DehydratedData } from 'src/devtools/views/Components/types';
|
||||
|
||||
export function cleanForBridge(data: Object | null): DehydratedData | null {
|
||||
export function cleanForBridge(
|
||||
data: Object | null,
|
||||
isPathWhitelisted: (path: Array<string | number>) => boolean,
|
||||
path?: Array<string | number> = []
|
||||
): DehydratedData | null {
|
||||
if (data !== null) {
|
||||
const cleaned = [];
|
||||
|
||||
return {
|
||||
data: dehydrate(data, cleaned),
|
||||
data: dehydrate(data, cleaned, path, isPathWhitelisted),
|
||||
cleaned,
|
||||
};
|
||||
} else {
|
||||
@@ -23,6 +27,7 @@ export function copyWithSet(
|
||||
value: any,
|
||||
index: number = 0
|
||||
): Object | Array<any> {
|
||||
console.log('[utils] copyWithSet()', obj, path, index, value);
|
||||
if (index >= path.length) {
|
||||
return value;
|
||||
}
|
||||
@@ -32,21 +37,3 @@ export function copyWithSet(
|
||||
updated[key] = copyWithSet(obj[key], path, value, index + 1);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function setInObject(
|
||||
object: Object,
|
||||
path: Array<string | number>,
|
||||
value: any
|
||||
) {
|
||||
const last = path.pop();
|
||||
if (object != null) {
|
||||
const parent: Object = path.reduce(
|
||||
// $FlowFixMe
|
||||
(reduced, attribute) => reduced[attribute],
|
||||
object
|
||||
);
|
||||
if (parent) {
|
||||
parent[last] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -4,7 +4,7 @@ import EventEmitter from 'events';
|
||||
|
||||
import type { ComponentFilter, Wall } from './types';
|
||||
import type {
|
||||
InspectedElement,
|
||||
InspectedElementPayload,
|
||||
OwnersList,
|
||||
ProfilingDataBackend,
|
||||
RendererID,
|
||||
@@ -43,6 +43,11 @@ type OverrideSuspense = {|
|
||||
forceFallback: boolean,
|
||||
|};
|
||||
|
||||
type InspectElementParams = {|
|
||||
...ElementAndRendererID,
|
||||
path?: Array<string | number>,
|
||||
|};
|
||||
|
||||
export default class Bridge extends EventEmitter<{|
|
||||
captureScreenshot: [{| commitIndex: number, rootID: number |}],
|
||||
clearHighlightedElementInDOM: [],
|
||||
@@ -51,8 +56,8 @@ export default class Bridge extends EventEmitter<{|
|
||||
getProfilingStatus: [],
|
||||
highlightElementInDOM: [HighlightElementInDOM],
|
||||
init: [],
|
||||
inspectElement: [ElementAndRendererID],
|
||||
inspectedElement: [InspectedElement | number | null],
|
||||
inspectElement: [InspectElementParams],
|
||||
inspectedElement: [InspectedElementPayload],
|
||||
isBackendStorageAPISupported: [boolean],
|
||||
logElementToConsole: [ElementAndRendererID],
|
||||
operations: [Uint32Array],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
|
||||
@@ -15,14 +15,10 @@ export default function ExpandCollapseToggle({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: ExpandCollapseToggleProps) {
|
||||
const handleClick = useCallback(() => {
|
||||
setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
}, [setIsOpen]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.ExpandCollapseToggle}
|
||||
onClick={handleClick}
|
||||
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
|
||||
title={`${isOpen ? 'Collapse' : 'Expand'} prop value`}
|
||||
>
|
||||
<ButtonIcon type={isOpen ? 'expanded' : 'collapsed'} />
|
||||
|
||||
@@ -7,11 +7,13 @@ import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import EditableValue from './EditableValue';
|
||||
import ExpandCollapseToggle from './ExpandCollapseToggle';
|
||||
import { InspectedElementContext } from './InspectedElementContext';
|
||||
import KeyValue from './KeyValue';
|
||||
import { serializeHooksForCopy } from '../utils';
|
||||
import styles from './HooksTree.css';
|
||||
import { meta } from '../../../hydration';
|
||||
|
||||
import type { InspectPath } from './SelectedElement';
|
||||
import type { HooksNode, HooksTree } from 'src/backend/types';
|
||||
|
||||
type HooksTreeViewProps = {|
|
||||
@@ -21,6 +23,13 @@ type HooksTreeViewProps = {|
|
||||
|};
|
||||
|
||||
export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
|
||||
const { getInspectedElementPath } = useContext(InspectedElementContext);
|
||||
const inspectPath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getInspectedElementPath(id, ['hooks', ...path]);
|
||||
},
|
||||
[getInspectedElementPath, id]
|
||||
);
|
||||
const handleCopy = useCallback(() => copy(serializeHooksForCopy(hooks)), [
|
||||
hooks,
|
||||
]);
|
||||
@@ -36,7 +45,13 @@ export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
<InnerHooksTreeView canEditHooks={canEditHooks} hooks={hooks} id={id} />
|
||||
<InnerHooksTreeView
|
||||
canEditHooks={canEditHooks}
|
||||
hooks={hooks}
|
||||
id={id}
|
||||
inspectPath={inspectPath}
|
||||
path={[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,12 +61,16 @@ type InnerHooksTreeViewProps = {|
|
||||
canEditHooks: boolean,
|
||||
hooks: HooksTree,
|
||||
id: number,
|
||||
inspectPath: InspectPath,
|
||||
path: Array<string | number>,
|
||||
|};
|
||||
|
||||
export function InnerHooksTreeView({
|
||||
canEditHooks,
|
||||
hooks,
|
||||
id,
|
||||
inspectPath,
|
||||
path,
|
||||
}: InnerHooksTreeViewProps) {
|
||||
// $FlowFixMe "Missing type annotation for U" whatever that means
|
||||
return hooks.map((hook, index) => (
|
||||
@@ -60,6 +79,8 @@ export function InnerHooksTreeView({
|
||||
canEditHooks={canEditHooks}
|
||||
hook={hooks[index]}
|
||||
id={id}
|
||||
inspectPath={inspectPath}
|
||||
path={path.concat([index])}
|
||||
/>
|
||||
));
|
||||
}
|
||||
@@ -68,10 +89,17 @@ type HookViewProps = {|
|
||||
canEditHooks: boolean,
|
||||
hook: HooksNode,
|
||||
id: number,
|
||||
path?: Array<any>,
|
||||
inspectPath: InspectPath,
|
||||
path: Array<string | number>,
|
||||
|};
|
||||
|
||||
function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
function HookView({
|
||||
canEditHooks,
|
||||
hook,
|
||||
id,
|
||||
inspectPath,
|
||||
path,
|
||||
}: HookViewProps) {
|
||||
const { name, id: hookID, isStateEditable, subHooks, value } = hook;
|
||||
|
||||
const bridge = useContext(BridgeContext);
|
||||
@@ -86,7 +114,9 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
|
||||
if (hook.hasOwnProperty(meta.inspected)) {
|
||||
// This Hook is too deep and hasn't been hydrated.
|
||||
// TODO: show UI to load its data.
|
||||
if (__DEV__) {
|
||||
console.warn('Unexpected dehydrated hook; this is a DevTools error.');
|
||||
}
|
||||
return (
|
||||
<div className={styles.Hook}>
|
||||
<div className={styles.NameValueRow}>
|
||||
@@ -96,8 +126,6 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO Add click and key handlers for toggling element open/close state.
|
||||
|
||||
const isCustomHook = subHooks.length > 0;
|
||||
|
||||
const type = typeof value;
|
||||
@@ -125,6 +153,24 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
}
|
||||
|
||||
if (isCustomHook) {
|
||||
const subHooksView = Array.isArray(subHooks) ? (
|
||||
<InnerHooksTreeView
|
||||
canEditHooks={canEditHooks}
|
||||
hooks={subHooks}
|
||||
id={id}
|
||||
inspectPath={inspectPath}
|
||||
path={path.concat(['subHooks'])}
|
||||
/>
|
||||
) : (
|
||||
<KeyValue
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name="subHooks"
|
||||
path={path.concat(['subHooks'])}
|
||||
value={subHooks}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isComplexDisplayValue) {
|
||||
return (
|
||||
<div className={styles.Hook}>
|
||||
@@ -135,12 +181,14 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.Children} hidden={!isOpen}>
|
||||
<KeyValue depth={1} name="DebugValue" value={value} />
|
||||
<InnerHooksTreeView
|
||||
canEditHooks={canEditHooks}
|
||||
hooks={subHooks}
|
||||
id={id}
|
||||
<KeyValue
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name="DebugValue"
|
||||
path={path.concat(['value'])}
|
||||
value={value}
|
||||
/>
|
||||
{subHooksView}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -156,11 +204,7 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
<span className={styles.Value}>{displayValue}</span>
|
||||
</div>
|
||||
<div className={styles.Children} hidden={!isOpen}>
|
||||
<InnerHooksTreeView
|
||||
canEditHooks={canEditHooks}
|
||||
hooks={subHooks}
|
||||
id={id}
|
||||
/>
|
||||
{subHooksView}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -169,12 +213,16 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
let overrideValueFn = null;
|
||||
// TODO Maybe read editable value from debug hook?
|
||||
if (canEditHooks && isStateEditable) {
|
||||
overrideValueFn = (path: Array<string | number>, value: any) => {
|
||||
overrideValueFn = (absolutePath: Array<string | number>, value: any) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('overrideHookState', {
|
||||
id,
|
||||
hookID,
|
||||
path,
|
||||
// Hooks override function expects a relative path for the specified hook (id),
|
||||
// starting with its id within the (flat) hooks list structure.
|
||||
// This relative path does not include the fake tree structure DevTools uses for display,
|
||||
// so it's important that we remove that part of the path before sending the update.
|
||||
path: absolutePath.slice(path.length + 1),
|
||||
rendererID,
|
||||
value,
|
||||
});
|
||||
@@ -186,8 +234,10 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
|
||||
<div className={styles.Hook}>
|
||||
<KeyValue
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path.concat(['value'])}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,17 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
||||
import { createResource } from '../../cache';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { hydrate } from 'src/hydration';
|
||||
import { hydrate, fillInPath } from 'src/hydration';
|
||||
import { TreeStateContext } from './TreeContext';
|
||||
import { separateDisplayNameAndHOCs } from 'src/utils';
|
||||
|
||||
import type { InspectedElement as InspectedElementBackend } from 'src/backend/types';
|
||||
import type {
|
||||
InspectedElement as InspectedElementBackend,
|
||||
InspectedElementPayload,
|
||||
} from 'src/backend/types';
|
||||
import type {
|
||||
DehydratedData,
|
||||
Element,
|
||||
@@ -22,8 +26,17 @@ import type {
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource, Thenable } from '../../cache';
|
||||
|
||||
export type GetInspectedElementPath = (
|
||||
id: number,
|
||||
path: Array<string | number>
|
||||
) => void;
|
||||
export type GetInspectedElement = (
|
||||
id: number
|
||||
) => InspectedElementFrontend | null;
|
||||
|
||||
type Context = {|
|
||||
read(id: number): InspectedElementFrontend | null,
|
||||
getInspectedElementPath: GetInspectedElementPath,
|
||||
getInspectedElement: GetInspectedElement,
|
||||
|};
|
||||
|
||||
const InspectedElementContext = createContext<Context>(((null: any): Context));
|
||||
@@ -68,7 +81,16 @@ function InspectedElementContextController({ children }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const read = useCallback(
|
||||
// Ask the backend to fill in a "dehydrated" path; this will result in a "inspectedElement".
|
||||
const getInspectedElementPath = useCallback<GetInspectedElementPath>(
|
||||
(id: number, path: Array<string | number>) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('inspectElement', { id, path, rendererID });
|
||||
},
|
||||
[bridge, store]
|
||||
);
|
||||
|
||||
const getInspectedElement = useCallback<GetInspectedElement>(
|
||||
(id: number) => {
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
@@ -85,72 +107,117 @@ function InspectedElementContextController({ children }: Props) {
|
||||
// would itself be blocked by the same render that suspends (waiting for the data).
|
||||
const { selectedElementID } = useContext(TreeStateContext);
|
||||
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [
|
||||
currentlyInspectedElement,
|
||||
setCurrentlyInspectedElement,
|
||||
] = useState<InspectedElementFrontend | null>(null);
|
||||
|
||||
// This effect handler invalidates the suspense cache and schedules rendering updates with React.
|
||||
useEffect(() => {
|
||||
const onInspectedElement = (
|
||||
data: InspectedElementBackend | number | null
|
||||
) => {
|
||||
// A null value means that the element no longer exists in the backend.
|
||||
// If it's the same element that's currently selected, that selection will be removed once the Store updates.
|
||||
// If it's not- then we can just ignore it anyway.
|
||||
// Either way there is nothing we need to do in this case.
|
||||
// A numeric value indicates that the element hasn't changed since we last requested its data,
|
||||
// in which case we don't need to invalidate the cache and re-render anything in the DevTools.
|
||||
if (data !== null && typeof data === 'object') {
|
||||
const id = data.id;
|
||||
const onInspectedElement = (data: InspectedElementPayload) => {
|
||||
const { id } = data;
|
||||
|
||||
const inspectedElement: InspectedElementFrontend = {
|
||||
canEditFunctionProps: data.canEditFunctionProps,
|
||||
canEditHooks: data.canEditHooks,
|
||||
canToggleSuspense: data.canToggleSuspense,
|
||||
canViewSource: data.canViewSource,
|
||||
id: data.id,
|
||||
source: data.source,
|
||||
type: data.type,
|
||||
owners:
|
||||
data.owners === null
|
||||
? null
|
||||
: data.owners.map(owner => {
|
||||
const [
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
] = separateDisplayNameAndHOCs(owner.displayName, owner.type);
|
||||
return {
|
||||
...owner,
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
};
|
||||
}),
|
||||
context: hydrateHelper(data.context),
|
||||
events: hydrateHelper(data.events),
|
||||
hooks: hydrateHelper(data.hooks),
|
||||
props: hydrateHelper(data.props),
|
||||
state: hydrateHelper(data.state),
|
||||
};
|
||||
let element;
|
||||
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(element);
|
||||
request.resolveFn(inspectedElement);
|
||||
} else {
|
||||
resource.write(element, inspectedElement);
|
||||
switch (data.type) {
|
||||
case 'no-change':
|
||||
case 'not-found':
|
||||
// No-op
|
||||
break;
|
||||
case 'hydrated-path':
|
||||
// Merge new data into previous object and invalidate cache
|
||||
element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
if (currentlyInspectedElement != null) {
|
||||
const value = hydrateHelper(data.value, data.path);
|
||||
const inspectedElement = { ...currentlyInspectedElement };
|
||||
|
||||
// Schedule update with React if the curently-selected element has been invalidated.
|
||||
if (id === selectedElementID) {
|
||||
setCount(count => count + 1);
|
||||
fillInPath(inspectedElement, data.path, value);
|
||||
|
||||
resource.write(element, inspectedElement);
|
||||
|
||||
// Schedule update with React if the curently-selected element has been invalidated.
|
||||
if (id === selectedElementID) {
|
||||
setCurrentlyInspectedElement(inspectedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'full-data':
|
||||
const {
|
||||
canEditFunctionProps,
|
||||
canEditHooks,
|
||||
canToggleSuspense,
|
||||
canViewSource,
|
||||
source,
|
||||
type,
|
||||
owners,
|
||||
context,
|
||||
events,
|
||||
hooks,
|
||||
props,
|
||||
state,
|
||||
} = ((data.value: any): InspectedElementBackend);
|
||||
|
||||
const inspectedElement: InspectedElementFrontend = {
|
||||
canEditFunctionProps,
|
||||
canEditHooks,
|
||||
canToggleSuspense,
|
||||
canViewSource,
|
||||
id,
|
||||
source,
|
||||
type,
|
||||
owners:
|
||||
owners === null
|
||||
? null
|
||||
: owners.map(owner => {
|
||||
const [
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
] = separateDisplayNameAndHOCs(
|
||||
owner.displayName,
|
||||
owner.type
|
||||
);
|
||||
return {
|
||||
...owner,
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
};
|
||||
}),
|
||||
context: hydrateHelper(context),
|
||||
events: hydrateHelper(events),
|
||||
hooks: hydrateHelper(hooks),
|
||||
props: hydrateHelper(props),
|
||||
state: hydrateHelper(state),
|
||||
};
|
||||
|
||||
element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(element);
|
||||
batchedUpdates(() => {
|
||||
request.resolveFn(inspectedElement);
|
||||
setCurrentlyInspectedElement(inspectedElement);
|
||||
});
|
||||
} else {
|
||||
resource.write(element, inspectedElement);
|
||||
|
||||
// Schedule update with React if the curently-selected element has been invalidated.
|
||||
if (id === selectedElementID) {
|
||||
setCurrentlyInspectedElement(inspectedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
return () => bridge.removeListener('inspectedElement', onInspectedElement);
|
||||
}, [bridge, selectedElementID, store]);
|
||||
}, [bridge, currentlyInspectedElement, selectedElementID, store]);
|
||||
|
||||
// This effect handler polls for updates on the currently selected element.
|
||||
useEffect(() => {
|
||||
@@ -175,15 +242,20 @@ function InspectedElementContextController({ children }: Props) {
|
||||
// Update the $r variable.
|
||||
bridge.send('selectElement', { id: selectedElementID, rendererID });
|
||||
|
||||
const onInspectedElement = (
|
||||
data: InspectedElementBackend | number | null
|
||||
) => {
|
||||
if (data !== null) {
|
||||
// If this is the element we requested, wait a little bit and then ask for an update.
|
||||
if (data === selectedElementID) {
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
} else if (typeof data === 'object' && data.id === selectedElementID) {
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
const onInspectedElement = (data: InspectedElementPayload) => {
|
||||
// If this is the element we requested, wait a little bit and then ask for another update.
|
||||
if (data.id === selectedElementID) {
|
||||
switch (data.type) {
|
||||
case 'no-change':
|
||||
case 'full-data':
|
||||
case 'hydrated-path':
|
||||
if (timeoutID !== null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -200,10 +272,10 @@ function InspectedElementContextController({ children }: Props) {
|
||||
}, [bridge, selectedElementID, store]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ read }),
|
||||
// Count is used to invalidate the cache and schedule an update with React.
|
||||
() => ({ getInspectedElement, getInspectedElementPath }),
|
||||
// InspectedElement is used to invalidate the cache and schedule an update with React.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[count, read]
|
||||
[currentlyInspectedElement, getInspectedElement, getInspectedElementPath]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -213,9 +285,23 @@ function InspectedElementContextController({ children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
function hydrateHelper(
|
||||
dehydratedData: DehydratedData | null,
|
||||
path?: Array<string | number>
|
||||
): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
let { cleaned, data } = dehydratedData;
|
||||
|
||||
if (path) {
|
||||
const { length } = path;
|
||||
if (length > 0) {
|
||||
// Hydration helper requires full paths, but inspection dehydrates with relative paths.
|
||||
// In that event it's important that we adjust the "cleaned" paths to match.
|
||||
cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
|
||||
}
|
||||
}
|
||||
|
||||
return hydrate(data, cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ import KeyValue from './KeyValue';
|
||||
import { serializeDataForCopy } from '../utils';
|
||||
import styles from './InspectedElementTree.css';
|
||||
|
||||
import type { InspectPath } from './SelectedElement';
|
||||
|
||||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type Props = {|
|
||||
data: Object | null,
|
||||
inspectPath?: InspectPath,
|
||||
label: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
showWhenEmpty?: boolean,
|
||||
@@ -19,6 +22,7 @@ type Props = {|
|
||||
|
||||
export default function InspectedElementTree({
|
||||
data,
|
||||
inspectPath,
|
||||
label,
|
||||
overrideValueFn,
|
||||
showWhenEmpty = false,
|
||||
@@ -32,7 +36,6 @@ export default function InspectedElementTree({
|
||||
if (isEmpty && !showWhenEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
// TODO Add click and key handlers for toggling element open/close state.
|
||||
return (
|
||||
<div className={styles.InspectedElementTree}>
|
||||
<div className={styles.HeaderRow}>
|
||||
@@ -49,6 +52,7 @@ export default function InspectedElementTree({
|
||||
<KeyValue
|
||||
key={name}
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={[name]}
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
|
||||
.Value {
|
||||
color: var(--color-attribute-value);
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.None {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { Element } from 'react';
|
||||
import EditableValue from './EditableValue';
|
||||
import ExpandCollapseToggle from './ExpandCollapseToggle';
|
||||
@@ -8,31 +8,51 @@ import { getMetaValueLabel } from '../utils';
|
||||
import { meta } from '../../../hydration';
|
||||
import styles from './KeyValue.css';
|
||||
|
||||
import type { InspectPath } from './SelectedElement';
|
||||
|
||||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type KeyValueProps = {|
|
||||
depth: number,
|
||||
hidden?: boolean,
|
||||
inspectPath?: InspectPath,
|
||||
name: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
path?: Array<any>,
|
||||
path: Array<any>,
|
||||
value: any,
|
||||
|};
|
||||
|
||||
export default function KeyValue({
|
||||
depth,
|
||||
inspectPath,
|
||||
hidden,
|
||||
name,
|
||||
overrideValueFn,
|
||||
path = [],
|
||||
path,
|
||||
value,
|
||||
}: KeyValueProps) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const prevIsOpenRef = useRef(isOpen);
|
||||
|
||||
const toggleIsOpen = useCallback(
|
||||
() => setIsOpen(prevIsOpen => !prevIsOpen),
|
||||
[]
|
||||
);
|
||||
const isInspectable =
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value[meta.inspectable] &&
|
||||
value[meta.size] !== 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isInspectable &&
|
||||
isOpen &&
|
||||
!prevIsOpenRef.current &&
|
||||
typeof inspectPath === 'function'
|
||||
) {
|
||||
inspectPath(path);
|
||||
}
|
||||
prevIsOpenRef.current = isOpen;
|
||||
}, [inspectPath, isInspectable, isOpen, path]);
|
||||
|
||||
const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
|
||||
|
||||
const dataType = typeof value;
|
||||
const isSimpleType =
|
||||
@@ -78,11 +98,19 @@ export default function KeyValue({
|
||||
</div>
|
||||
);
|
||||
} else if (value.hasOwnProperty(meta.type)) {
|
||||
// TODO Is this type even necessary? Can we just drop it?
|
||||
children = (
|
||||
<div key="root" className={styles.Item} hidden={hidden} style={style}>
|
||||
<div className={styles.ExpandCollapseToggleSpacer} />
|
||||
<span className={styles.Name}>{name}</span>
|
||||
{isInspectable ? (
|
||||
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
) : (
|
||||
<div className={styles.ExpandCollapseToggleSpacer} />
|
||||
)}
|
||||
<span
|
||||
className={styles.Name}
|
||||
onClick={isInspectable ? toggleIsOpen : undefined}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className={styles.Value}>{getMetaValueLabel(value)}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -94,6 +122,7 @@ export default function KeyValue({
|
||||
<KeyValue
|
||||
key={index}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
hidden={hidden || !isOpen}
|
||||
name={index}
|
||||
overrideValueFn={overrideValueFn}
|
||||
@@ -129,6 +158,7 @@ export default function KeyValue({
|
||||
<KeyValue
|
||||
key={name}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
hidden={hidden || !isOpen}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import styles from './SelectedElement.css';
|
||||
|
||||
import type { GetInspectedElementPath } from './InspectedElementContext';
|
||||
import type { Element, InspectedElement } from './types';
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
@@ -38,7 +39,9 @@ export default function SelectedElement(_: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
|
||||
|
||||
const { read } = useContext(InspectedElementContext);
|
||||
const { getInspectedElementPath, getInspectedElement } = useContext(
|
||||
InspectedElementContext
|
||||
);
|
||||
|
||||
const element =
|
||||
inspectedElementID !== null
|
||||
@@ -46,7 +49,7 @@ export default function SelectedElement(_: Props) {
|
||||
: null;
|
||||
|
||||
const inspectedElement =
|
||||
inspectedElementID != null ? read(inspectedElementID) : null;
|
||||
inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
|
||||
|
||||
const highlightElement = useCallback(() => {
|
||||
if (element !== null && inspectedElementID !== null) {
|
||||
@@ -200,7 +203,11 @@ export default function SelectedElement(_: Props) {
|
||||
|
||||
{inspectedElement !== null && (
|
||||
<InspectedElementView
|
||||
key={
|
||||
inspectedElementID /* Force reset when seleted Element changes */
|
||||
}
|
||||
element={element}
|
||||
getInspectedElementPath={getInspectedElementPath}
|
||||
inspectedElement={inspectedElement}
|
||||
/>
|
||||
)}
|
||||
@@ -208,8 +215,11 @@ export default function SelectedElement(_: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export type InspectPath = (path: Array<string | number>) => void;
|
||||
|
||||
type InspectedElementViewProps = {|
|
||||
element: Element,
|
||||
getInspectedElementPath: GetInspectedElementPath,
|
||||
inspectedElement: InspectedElement,
|
||||
|};
|
||||
|
||||
@@ -217,6 +227,7 @@ const IS_SUSPENDED = 'Suspended';
|
||||
|
||||
function InspectedElementView({
|
||||
element,
|
||||
getInspectedElementPath,
|
||||
inspectedElement,
|
||||
}: InspectedElementViewProps) {
|
||||
const { id, type } = element;
|
||||
@@ -236,6 +247,25 @@ function InspectedElementView({
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const inspectContextPath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getInspectedElementPath(id, ['context', ...path]);
|
||||
},
|
||||
[getInspectedElementPath, id]
|
||||
);
|
||||
const inspectPropsPath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getInspectedElementPath(id, ['props', ...path]);
|
||||
},
|
||||
[getInspectedElementPath, id]
|
||||
);
|
||||
const inspectStatePath = useCallback(
|
||||
(path: Array<string | number>) => {
|
||||
getInspectedElementPath(id, ['state', ...path]);
|
||||
},
|
||||
[getInspectedElementPath, id]
|
||||
);
|
||||
|
||||
let overrideContextFn = null;
|
||||
let overridePropsFn = null;
|
||||
let overrideStateFn = null;
|
||||
@@ -279,6 +309,7 @@ function InspectedElementView({
|
||||
<InspectedElementTree
|
||||
label="props"
|
||||
data={props}
|
||||
inspectPath={inspectPropsPath}
|
||||
overrideValueFn={overridePropsFn}
|
||||
showWhenEmpty
|
||||
/>
|
||||
@@ -294,6 +325,7 @@ function InspectedElementView({
|
||||
<InspectedElementTree
|
||||
label="state"
|
||||
data={state}
|
||||
inspectPath={inspectStatePath}
|
||||
overrideValueFn={overrideStateFn}
|
||||
/>
|
||||
)}
|
||||
@@ -301,6 +333,7 @@ function InspectedElementView({
|
||||
<InspectedElementTree
|
||||
label="context"
|
||||
data={context}
|
||||
inspectPath={inspectContextPath}
|
||||
overrideValueFn={overrideContextFn}
|
||||
/>
|
||||
{events !== null && events.length > 0 && <EventsTree events={events} />}
|
||||
|
||||
@@ -83,6 +83,6 @@ export type InspectedElement = {|
|
||||
// TODO: Add profiling type
|
||||
|
||||
export type DehydratedData = {|
|
||||
cleaned: Array<Array<string>>,
|
||||
cleaned: Array<Array<string | number>>,
|
||||
data: Object,
|
||||
|};
|
||||
|
||||
@@ -91,7 +91,7 @@ export function getMetaValueLabel(data: Object): string | null {
|
||||
case 'data_view':
|
||||
case 'array':
|
||||
case 'typed_array':
|
||||
return `${name}[${data[meta.meta].length}]`;
|
||||
return `${name}[${data[meta.size]}]`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
+110
-64
@@ -16,27 +16,32 @@ import {
|
||||
StrictMode,
|
||||
Suspense,
|
||||
} from 'react-is';
|
||||
import { getDisplayName } from './utils';
|
||||
import { getDisplayName, getInObject, setInObject } from './utils';
|
||||
|
||||
export const meta = {
|
||||
name: Symbol('name'),
|
||||
type: Symbol('type'),
|
||||
inspectable: Symbol('inspectable'),
|
||||
inspected: Symbol('inspected'),
|
||||
meta: Symbol('meta'),
|
||||
proto: Symbol('proto'),
|
||||
name: Symbol('name'),
|
||||
readonly: Symbol('readonly'),
|
||||
size: Symbol('size'),
|
||||
type: Symbol('type'),
|
||||
};
|
||||
|
||||
type Dehydrated = {|
|
||||
inspectable: boolean,
|
||||
name: string | null,
|
||||
readonly?: boolean,
|
||||
size?: number,
|
||||
type: string,
|
||||
|};
|
||||
|
||||
// This threshold determines the depth at which the bridge "dehydrates" nested data.
|
||||
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
|
||||
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
|
||||
// We tried reducing this value from 2 to 1 to improve performance:
|
||||
// https://github.com/facebook/react-devtools/issues/1200
|
||||
// But this caused problems with the Profiler's interaction tracing output.
|
||||
// Because React mutates Fibers, profiling data that is dehydrated for old commits–
|
||||
// will not be available later from within the Profiler.
|
||||
// This impacts props/state as well as Interactions.
|
||||
// https://github.com/facebook/react-devtools/issues/1262
|
||||
const LEVEL_THRESHOLD = 6;
|
||||
//
|
||||
// Reducing this threshold will improve the speed of initial component inspection,
|
||||
// but may decrease the responsiveness of expanding objects/arrays to inspect further.
|
||||
const LEVEL_THRESHOLD = 2;
|
||||
|
||||
/**
|
||||
* Get a enhanced/artificial type string based on the object instance
|
||||
@@ -84,29 +89,33 @@ function getPropType(data: Object): string | null {
|
||||
*/
|
||||
function createDehydrated(
|
||||
type: string,
|
||||
inspectable: boolean,
|
||||
data: Object,
|
||||
cleaned: Array<Array<string>>,
|
||||
path: Array<string>
|
||||
): Object {
|
||||
const meta = {};
|
||||
|
||||
if (type === 'array' || type === 'typed_array') {
|
||||
meta.length = data.length;
|
||||
}
|
||||
if (type === 'iterator' || type === 'typed_array') {
|
||||
meta.readOnly = true;
|
||||
}
|
||||
|
||||
cleaned: Array<Array<string | number>>,
|
||||
path: Array<string | number>
|
||||
): Dehydrated {
|
||||
cleaned.push(path);
|
||||
|
||||
return {
|
||||
const dehydrated: Dehydrated = {
|
||||
inspectable,
|
||||
type,
|
||||
meta,
|
||||
name:
|
||||
!data.constructor || data.constructor.name === 'Object'
|
||||
? ''
|
||||
: data.constructor.name,
|
||||
};
|
||||
|
||||
if (type === 'array' || type === 'typed_array') {
|
||||
dehydrated.size = data.length;
|
||||
} else if (type === 'object') {
|
||||
dehydrated.size = Object.keys(data).length;
|
||||
}
|
||||
|
||||
if (type === 'iterator' || type === 'typed_array') {
|
||||
dehydrated.readonly = true;
|
||||
}
|
||||
|
||||
return dehydrated;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,16 +138,18 @@ function createDehydrated(
|
||||
*/
|
||||
export function dehydrate(
|
||||
data: Object,
|
||||
cleaned: Array<Array<string>>,
|
||||
path?: Array<string> = [],
|
||||
cleaned: Array<Array<string | number>>,
|
||||
path: Array<string | number>,
|
||||
isPathWhitelisted: (path: Array<string | number>) => boolean,
|
||||
level?: number = 0
|
||||
): string | Object {
|
||||
): string | Dehydrated | { [key: string]: string | Dehydrated } {
|
||||
const type = getPropType(data);
|
||||
|
||||
switch (type) {
|
||||
case 'html_element':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.tagName,
|
||||
type: 'html_element',
|
||||
};
|
||||
@@ -146,6 +157,7 @@ export function dehydrate(
|
||||
case 'function':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.name,
|
||||
type: 'function',
|
||||
};
|
||||
@@ -158,8 +170,9 @@ export function dehydrate(
|
||||
case 'symbol':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
type: 'symbol',
|
||||
inspectable: false,
|
||||
name: data.toString(),
|
||||
type: 'symbol',
|
||||
};
|
||||
|
||||
// React Elements aren't very inspector-friendly,
|
||||
@@ -167,6 +180,7 @@ export function dehydrate(
|
||||
case 'react_element':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: getDisplayNameForReactElement(data),
|
||||
type: 'react_element',
|
||||
};
|
||||
@@ -176,48 +190,55 @@ export function dehydrate(
|
||||
case 'data_view':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
type,
|
||||
inspectable: false,
|
||||
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
|
||||
meta: {
|
||||
length: data.byteLength,
|
||||
uninspectable: true,
|
||||
},
|
||||
size: data.byteLength,
|
||||
type,
|
||||
};
|
||||
|
||||
case 'array':
|
||||
if (level > LEVEL_THRESHOLD) {
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
const arrayPathCheck = isPathWhitelisted(path);
|
||||
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
|
||||
return createDehydrated(type, true, data, cleaned, path);
|
||||
}
|
||||
return data.map((item, i) =>
|
||||
dehydrate(item, cleaned, path.concat([i]), level + 1)
|
||||
dehydrate(
|
||||
item,
|
||||
cleaned,
|
||||
path.concat([i]),
|
||||
isPathWhitelisted,
|
||||
arrayPathCheck ? 1 : level + 1
|
||||
)
|
||||
);
|
||||
|
||||
case 'typed_array':
|
||||
case 'iterator':
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
return createDehydrated(type, false, data, cleaned, path);
|
||||
|
||||
case 'date':
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
name: data.toString(),
|
||||
type: 'date',
|
||||
meta: {
|
||||
uninspectable: true,
|
||||
},
|
||||
};
|
||||
|
||||
case 'object':
|
||||
if (level > LEVEL_THRESHOLD) {
|
||||
return createDehydrated(type, data, cleaned, path);
|
||||
const objectPathCheck = isPathWhitelisted(path);
|
||||
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
|
||||
return createDehydrated(type, true, data, cleaned, path);
|
||||
} else {
|
||||
const res = {};
|
||||
const object = {};
|
||||
for (let name in data) {
|
||||
res[name] = dehydrate(
|
||||
object[name] = dehydrate(
|
||||
data[name],
|
||||
cleaned,
|
||||
path.concat([name]),
|
||||
level + 1
|
||||
isPathWhitelisted,
|
||||
objectPathCheck ? 1 : level + 1
|
||||
);
|
||||
}
|
||||
return res;
|
||||
return object;
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -225,24 +246,49 @@ export function dehydrate(
|
||||
}
|
||||
}
|
||||
|
||||
export function hydrate(data: Object, cleaned: Array<Array<string>>): Object {
|
||||
cleaned.forEach((path: Array<string>) => {
|
||||
const last = path.pop();
|
||||
const reduced: Object = path.reduce(
|
||||
(object: Object, attr: string) => (object ? object[attr] : (null: any)),
|
||||
data
|
||||
);
|
||||
if (!reduced || !reduced[last]) {
|
||||
export function fillInPath(
|
||||
object: Object,
|
||||
path: Array<string | number>,
|
||||
value: any
|
||||
) {
|
||||
const target = getInObject(object, path);
|
||||
if (target != null) {
|
||||
delete target[meta.inspectable];
|
||||
delete target[meta.inspected];
|
||||
delete target[meta.name];
|
||||
delete target[meta.readonly];
|
||||
delete target[meta.size];
|
||||
delete target[meta.type];
|
||||
}
|
||||
setInObject(object, path, value);
|
||||
}
|
||||
|
||||
export function hydrate(
|
||||
object: Object,
|
||||
cleaned: Array<Array<string | number>>
|
||||
): Object {
|
||||
cleaned.forEach((path: Array<string | number>) => {
|
||||
const length = path.length;
|
||||
const last = path[length - 1];
|
||||
const parent = getInObject(object, path.slice(0, length - 1));
|
||||
if (!parent || !parent[last]) {
|
||||
return;
|
||||
}
|
||||
const replace: { [key: Symbol]: boolean | string } = {};
|
||||
replace[meta.name] = reduced[last].name;
|
||||
replace[meta.type] = reduced[last].type;
|
||||
replace[meta.meta] = reduced[last].meta;
|
||||
replace[meta.inspected] = false;
|
||||
reduced[last] = replace;
|
||||
|
||||
const value = parent[last];
|
||||
|
||||
// Replace the string keys with Symbols so they're non-enumerable.
|
||||
const replaced: { [key: Symbol]: boolean | string } = {};
|
||||
replaced[meta.inspectable] = !!value.inspectable;
|
||||
replaced[meta.inspected] = false;
|
||||
replaced[meta.name] = value.name;
|
||||
replaced[meta.size] = value.size;
|
||||
replaced[meta.readonly] = !!value.readonly;
|
||||
replaced[meta.type] = value.type;
|
||||
|
||||
parent[last] = replaced;
|
||||
});
|
||||
return data;
|
||||
return object;
|
||||
}
|
||||
|
||||
export function getDisplayNameForReactElement(
|
||||
|
||||
@@ -245,3 +245,30 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getInObject(object: Object, path: Array<string | number>): any {
|
||||
return path.reduce((reduced: Object, attr: string | number): any => {
|
||||
if (typeof reduced === 'object' && reduced !== null) {
|
||||
return reduced[attr];
|
||||
} else if (Array.isArray(reduced)) {
|
||||
return reduced[attr];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, object);
|
||||
}
|
||||
|
||||
export function setInObject(
|
||||
object: Object,
|
||||
path: Array<string | number>,
|
||||
value: any
|
||||
) {
|
||||
const length = path.length;
|
||||
const last = path[length - 1];
|
||||
if (object != null) {
|
||||
const parent = getInObject(object, path.slice(0, length - 1));
|
||||
if (parent) {
|
||||
parent[last] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user