diff --git a/shells/dev/app/Hydration/index.js b/shells/dev/app/Hydration/index.js
new file mode 100644
index 0000000000..bf0cd66a52
--- /dev/null
+++ b/shells/dev/app/Hydration/index.js
@@ -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 (
+
+ Hydration
+ }
+ array_buffer={typedArray.buffer}
+ typed_array={typedArray}
+ date={new Date()}
+ array={arrayOfArrays}
+ object={objectOfObjects}
+ />
+
+
+ );
+}
+
+function DehydratableProps({ array, object }: any) {
+ return (
+
+ - array: {JSON.stringify(array, null, 2)}
+ - object: {JSON.stringify(object, null, 2)}
+
+ );
+}
+
+function DeepHooks(props: any) {
+ const foo = useOuterFoo();
+ const bar = useOuterBar();
+ const baz = useOuterBaz();
+ return (
+
+ - foo: {foo}
+ - bar: {bar}
+ - baz: {baz}
+
+ );
+}
diff --git a/shells/dev/app/index.js b/shells/dev/app/index.js
index 1a2336982c..b5f0923c1b 100644
--- a/shells/dev/app/index.js
+++ b/shells/dev/app/index.js
@@ -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);
diff --git a/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
index 2587fed2b4..1c9be05a4f 100644
--- a/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+++ b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
@@ -1,11 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`InspectedElementContext should inspect the currently selected element: 1: mount 1`] = `
-[root]
-
+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]
- ▾
-
+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]
-
-`;
-
-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]
-
+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,
diff --git a/src/__tests__/inspectedElementContext-test.js b/src/__tests__/inspectedElementContext-test.js
index 1a66f1dcd1..105a23135f 100644
--- a/src/__tests__/inspectedElementContext-test.js
+++ b/src/__tests__/inspectedElementContext-test.js
@@ -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(, container)
+ ReactDOM.render(, 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(
-
+
),
@@ -110,18 +112,17 @@ describe('InspectedElementContext', () => {
const container = document.createElement('div');
await utils.actAsync(
- () => ReactDOM.render(, container),
+ () => ReactDOM.render(, 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(
-
+
-
+
);
}, false);
- expect(inspectedElement).toMatchSnapshot('2: initial render');
+ expect(inspectedElement).toMatchSnapshot('1: initial render');
await utils.actAsync(
- () => ReactDOM.render(, container),
+ () => ReactDOM.render(, container),
false
);
@@ -151,11 +149,11 @@ describe('InspectedElementContext', () => {
() =>
renderer.update(
-
+
),
@@ -180,20 +178,19 @@ describe('InspectedElementContext', () => {
await utils.actAsync(() =>
ReactDOM.render(
-
+
,
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(
-
+
,
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(
+ }
+ 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(
+
+
+
+
+
+ ),
+ 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(, 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(
-
+
),
@@ -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(
+ ,
+ 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(
+
+
+
+
+
+ ),
+ 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(
+ ,
+ 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(
+
+
+
+
+
+ ),
+ 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(
+ ,
+ 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(
+ ,
+ 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(
+
+
+
+
+
+ ),
+ false
+ );
+ expect(getInspectedElementPath).not.toBeNull();
+ expect(inspectedElement).not.toBeNull();
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
+
+ TestUtils.act(() => {
+ ReactDOM.render(
+ ,
+ 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();
+ });
});
diff --git a/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
new file mode 100644
index 0000000000..db3aec6f29
--- /dev/null
+++ b/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
@@ -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
+},
+}
+`;
diff --git a/src/__tests__/legacy/inspectElement-test.js b/src/__tests__/legacy/inspectElement-test.js
new file mode 100644
index 0000000000..23e384e529
--- /dev/null
+++ b/src/__tests__/legacy/inspectElement-test.js
@@ -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
+ ): Promise