From ce65df7092ed69b45aa5787b708c1516b2c07427 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 17 Aug 2019 10:20:26 -0700 Subject: [PATCH] Added support for unserializable types (e.g. Set/Map, Immutable) --- .../InspectableElements.js | 4 +- .../dev/app/InspectableElements/MapAndSet.js | 19 -- .../UnserializableProps.js | 35 ++++ .../inspectedElementContext-test.js.snap | 73 +++++++- src/__tests__/inspectedElementContext-test.js | 171 +++++++++++++++--- .../__snapshots__/inspectElement-test.js.snap | 37 +++- src/__tests__/legacy/inspectElement-test.js | 121 ++++++++++--- src/backend/utils.js | 15 +- .../Components/InspectedElementContext.js | 9 +- src/devtools/views/Components/KeyValue.js | 26 ++- src/devtools/views/Components/types.js | 1 + src/hydration.js | 155 ++++++++++++++-- src/utils.js | 16 +- 13 files changed, 558 insertions(+), 124 deletions(-) delete mode 100644 shells/dev/app/InspectableElements/MapAndSet.js create mode 100644 shells/dev/app/InspectableElements/UnserializableProps.js diff --git a/shells/dev/app/InspectableElements/InspectableElements.js b/shells/dev/app/InspectableElements/InspectableElements.js index 80aa35f7b4..5428c3b663 100644 --- a/shells/dev/app/InspectableElements/InspectableElements.js +++ b/shells/dev/app/InspectableElements/InspectableElements.js @@ -1,10 +1,10 @@ // @flow import React, { Fragment } from 'react'; +import UnserializableProps from './UnserializableProps'; import Contexts from './Contexts'; import CustomHooks from './CustomHooks'; import CustomObject from './CustomObject'; -import MapAndSet from './MapAndSet'; import NestedProps from './NestedProps'; import SimpleValues from './SimpleValues'; @@ -15,7 +15,7 @@ export default function InspectableElements() {

Inspectable elements

- + diff --git a/shells/dev/app/InspectableElements/MapAndSet.js b/shells/dev/app/InspectableElements/MapAndSet.js deleted file mode 100644 index b81ac8686a..0000000000 --- a/shells/dev/app/InspectableElements/MapAndSet.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow - -import React from 'react'; - -const set = new Set(); -set.add('abc'); -set.add(123); - -const map = new Map(); -map.set('name', 'Brian'); -map.set('food', 'sushi'); - -export default function MapAndSet() { - return ; -} - -function ChildComponent(props: any) { - return null; -} diff --git a/shells/dev/app/InspectableElements/UnserializableProps.js b/shells/dev/app/InspectableElements/UnserializableProps.js new file mode 100644 index 0000000000..85f149e166 --- /dev/null +++ b/shells/dev/app/InspectableElements/UnserializableProps.js @@ -0,0 +1,35 @@ +// @flow + +import React from 'react'; +import Immutable from 'immutable'; + +const set = new Set(['abc', 123]); +const map = new Map([['name', 'Brian'], ['food', 'sushi']]); +const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); +const mapOfMaps = new Map([['first', map], ['second', map]]); +const typedArray = Int8Array.from([100, -100, 0]); +const immutable = Immutable.fromJS({ + a: [{ hello: 'there' }, 'fixed', true], + b: 123, + c: { + '1': 'xyz', + xyz: 1, + }, +}); + +export default function UnserializableProps() { + return ( + + ); +} + +function ChildComponent(props: any) { + return null; +} diff --git a/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap index afdbb89de0..712fdb59b0 100644 --- a/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap +++ b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap @@ -1,5 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": null, + "props": { + "set_of_sets": { + "0": {}, + "1": {} + } + }, + "state": null +} +`; + +exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": null, + "props": { + "set_of_sets": { + "0": { + "0": 1, + "1": 2, + "2": 3 + }, + "1": {} + } + }, + "state": null +} +`; + exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = ` { "id": 2, @@ -449,13 +485,38 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected "context": null, "hooks": null, "props": { - "html_element": {}, - "fn": {}, - "symbol": {}, - "react_element": {}, "array_buffer": {}, - "typed_array": {}, - "date": {} + "date": {}, + "fn": {}, + "html_element": {}, + "immutable": { + "0": {}, + "1": {}, + "2": {} + }, + "map": { + "0": {}, + "1": {} + }, + "map_of_maps": { + "0": {}, + "1": {} + }, + "react_element": {}, + "set": { + "0": "abc", + "1": 123 + }, + "set_of_sets": { + "0": {}, + "1": {} + }, + "symbol": {}, + "typed_array": { + "0": 100, + "1": -100, + "2": 0 + } }, "state": null } diff --git a/src/__tests__/inspectedElementContext-test.js b/src/__tests__/inspectedElementContext-test.js index c2594b2931..016f927784 100644 --- a/src/__tests__/inspectedElementContext-test.js +++ b/src/__tests__/inspectedElementContext-test.js @@ -390,23 +390,42 @@ describe('InspectedElementContext', () => { }); it('should support complex data types', async done => { + const Immutable = require('immutable'); + const Example = () => null; const div = document.createElement('div'); - const exmapleFunction = () => {}; - const typedArray = new Uint8Array(3); + const exampleFunction = () => {}; + const setShallow = new Set(['abc', 123]); + const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]); + const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); + const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]); + const typedArray = Int8Array.from([100, -100, 0]); + const immutableMap = Immutable.fromJS({ + a: [{ hello: 'there' }, 'fixed', true], + b: 123, + c: { + '1': 'xyz', + xyz: 1, + }, + }); const container = document.createElement('div'); await utils.actAsync(() => ReactDOM.render( } array_buffer={typedArray.buffer} - typed_array={typedArray} date={new Date()} + fn={exampleFunction} + html_element={div} + immutable={immutableMap} + map={mapShallow} + map_of_maps={mapOfMaps} + react_element={} + set={setShallow} + set_of_sets={setOfSets} + symbol={Symbol('symbol')} + typed_array={typedArray} />, container ) @@ -441,37 +460,77 @@ describe('InspectedElementContext', () => { expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`); const { - html_element, - fn, - symbol, - react_element, array_buffer, - typed_array, date, + fn, + html_element, + immutable, + map, + map_of_maps, + react_element, + set, + set_of_sets, + symbol, + typed_array, } = (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'); + expect(fn[meta.inspectable]).toBe(false); + expect(fn[meta.name]).toBe('exampleFunction'); + expect(fn[meta.type]).toBe('function'); + + expect(html_element[meta.inspectable]).toBe(false); + expect(html_element[meta.name]).toBe('DIV'); + expect(html_element[meta.type]).toBe('html_element'); + + expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type + expect(immutable[meta.name]).toBe('Map'); + expect(immutable[meta.type]).toBe('iterator'); + + expect(map[meta.inspectable]).toBeUndefined(); // Complex type + expect(map[meta.name]).toBe('Map'); + expect(map[meta.type]).toBe('iterator'); + expect(map[0][meta.type]).toBe('array'); + + expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type + expect(map_of_maps[meta.name]).toBe('Map'); + expect(map_of_maps[meta.type]).toBe('iterator'); + expect(map_of_maps[0][meta.type]).toBe('array'); + + expect(react_element[meta.inspectable]).toBe(false); + expect(react_element[meta.name]).toBe('span'); + expect(react_element[meta.type]).toBe('react_element'); + + expect(set[meta.inspectable]).toBeUndefined(); // Complex type + expect(set[meta.name]).toBe('Set'); + expect(set[meta.type]).toBe('iterator'); + expect(set[0]).toBe('abc'); + expect(set[1]).toBe(123); + + expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type + expect(set_of_sets[meta.name]).toBe('Set'); + expect(set_of_sets[meta.type]).toBe('iterator'); + expect(set_of_sets['0'][meta.inspectable]).toBe(true); + + expect(symbol[meta.inspectable]).toBe(false); + expect(symbol[meta.name]).toBe('Symbol(symbol)'); + expect(symbol[meta.type]).toBe('symbol'); + + expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type + expect(typed_array[meta.size]).toBe(3); + expect(typed_array[meta.name]).toBe('Int8Array'); + expect(typed_array[meta.type]).toBe('typed_array'); + expect(typed_array[0]).toBe(100); + expect(typed_array[1]).toBe(-100); + expect(typed_array[2]).toBe(0); + done(); }); @@ -663,6 +722,62 @@ describe('InspectedElementContext', () => { done(); }); + it('should dehydrate complex nested values when requested', async done => { + const Example = () => null; + + const container = document.createElement('div'); + await utils.actAsync(() => + ReactDOM.render( + , + container + ) + ); + + const id = ((store.getElementIDAtIndex(0): any): number); + + let 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(() => { + TestRenderer.act(() => { + getInspectedElementPath(id, ['props', 'set_of_sets', 0]); + jest.runOnlyPendingTimers(); + }); + }); + expect(inspectedElement).not.toBeNull(); + expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0'); + + done(); + }); + it('should include updates for nested values that were previously hydrated', async done => { const Example = () => null; diff --git a/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap index 1cfc30e884..b0d83ec678 100644 --- a/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap +++ b/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap @@ -126,13 +126,38 @@ Object { "context": {}, "hooks": null, "props": { - "html_element": {}, - "fn": {}, - "symbol": {}, - "react_element": {}, "array_buffer": {}, - "typed_array": {}, - "date": {} + "date": {}, + "fn": {}, + "html_element": {}, + "immutable": { + "0": {}, + "1": {}, + "2": {} + }, + "map": { + "0": {}, + "1": {} + }, + "map_of_maps": { + "0": {}, + "1": {} + }, + "react_element": {}, + "set": { + "0": "abc", + "1": 123 + }, + "set_of_sets": { + "0": {}, + "1": {} + }, + "symbol": {}, + "typed_array": { + "0": 100, + "1": -100, + "2": 0 + } }, "state": null }, diff --git a/src/__tests__/legacy/inspectElement-test.js b/src/__tests__/legacy/inspectElement-test.js index 7b8c2b04a9..8fbe51034d 100644 --- a/src/__tests__/legacy/inspectElement-test.js +++ b/src/__tests__/legacy/inspectElement-test.js @@ -23,7 +23,11 @@ describe('InspectedElementContext', () => { dehydratedData: DehydratedData | null ): Object | null { if (dehydratedData !== null) { - return hydrate(dehydratedData.data, dehydratedData.cleaned); + return hydrate( + dehydratedData.data, + dehydratedData.cleaned, + dehydratedData.unserializable + ); } else { return null; } @@ -132,22 +136,41 @@ describe('InspectedElementContext', () => { }); it('should support complex data types', async done => { + const Immutable = require('immutable'); + const Example = () => null; const div = document.createElement('div'); - const exmapleFunction = () => {}; - const typedArray = new Uint8Array(3); + const exampleFunction = () => {}; + const setShallow = new Set(['abc', 123]); + const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]); + const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); + const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]); + const typedArray = Int8Array.from([100, -100, 0]); + const immutableMap = Immutable.fromJS({ + a: [{ hello: 'there' }, 'fixed', true], + b: 123, + c: { + '1': 'xyz', + xyz: 1, + }, + }); act(() => ReactDOM.render( } array_buffer={typedArray.buffer} - typed_array={typedArray} date={new Date()} + fn={exampleFunction} + html_element={div} + immutable={immutableMap} + map={mapShallow} + map_of_maps={mapOfMaps} + react_element={} + set={setShallow} + set_of_sets={setOfSets} + symbol={Symbol('symbol')} + typed_array={typedArray} />, document.createElement('div') ) @@ -159,37 +182,77 @@ describe('InspectedElementContext', () => { expect(inspectedElement).toMatchSnapshot('1: Initial inspection'); const { - html_element, - fn, - symbol, - react_element, array_buffer, - typed_array, date, + fn, + html_element, + immutable, + map, + map_of_maps, + react_element, + set, + set_of_sets, + symbol, + typed_array, } = 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'); + expect(fn[meta.inspectable]).toBe(false); + expect(fn[meta.name]).toBe('exampleFunction'); + expect(fn[meta.type]).toBe('function'); + + expect(html_element[meta.inspectable]).toBe(false); + expect(html_element[meta.name]).toBe('DIV'); + expect(html_element[meta.type]).toBe('html_element'); + + expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type + expect(immutable[meta.name]).toBe('Map'); + expect(immutable[meta.type]).toBe('iterator'); + + expect(map[meta.inspectable]).toBeUndefined(); // Complex type + expect(map[meta.name]).toBe('Map'); + expect(map[meta.type]).toBe('iterator'); + expect(map[0][meta.type]).toBe('array'); + + expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type + expect(map_of_maps[meta.name]).toBe('Map'); + expect(map_of_maps[meta.type]).toBe('iterator'); + expect(map_of_maps[0][meta.type]).toBe('array'); + + expect(react_element[meta.inspectable]).toBe(false); + expect(react_element[meta.name]).toBe('span'); + expect(react_element[meta.type]).toBe('react_element'); + + expect(set[meta.inspectable]).toBeUndefined(); // Complex type + expect(set[meta.name]).toBe('Set'); + expect(set[meta.type]).toBe('iterator'); + expect(set[0]).toBe('abc'); + expect(set[1]).toBe(123); + + expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type + expect(set_of_sets[meta.name]).toBe('Set'); + expect(set_of_sets[meta.type]).toBe('iterator'); + expect(set_of_sets['0'][meta.inspectable]).toBe(true); + + expect(symbol[meta.inspectable]).toBe(false); + expect(symbol[meta.name]).toBe('Symbol(symbol)'); + expect(symbol[meta.type]).toBe('symbol'); + + expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type + expect(typed_array[meta.size]).toBe(3); + expect(typed_array[meta.name]).toBe('Int8Array'); + expect(typed_array[meta.type]).toBe('typed_array'); + expect(typed_array[0]).toBe(100); + expect(typed_array[1]).toBe(-100); + expect(typed_array[2]).toBe(0); + done(); }); diff --git a/src/backend/utils.js b/src/backend/utils.js index 020d841d46..6a71e7144a 100644 --- a/src/backend/utils.js +++ b/src/backend/utils.js @@ -10,11 +10,20 @@ export function cleanForBridge( path?: Array = [] ): DehydratedData | null { if (data !== null) { - const cleaned = []; + const cleanedPaths = []; + const unserializablePaths = []; + const cleanedData = dehydrate( + data, + cleanedPaths, + unserializablePaths, + path, + isPathWhitelisted + ); return { - data: dehydrate(data, cleaned, path, isPathWhitelisted), - cleaned, + data: cleanedData, + cleaned: cleanedPaths, + unserializable: unserializablePaths, }; } else { return null; diff --git a/src/devtools/views/Components/InspectedElementContext.js b/src/devtools/views/Components/InspectedElementContext.js index cc9d1f5969..bcdb83ca9f 100644 --- a/src/devtools/views/Components/InspectedElementContext.js +++ b/src/devtools/views/Components/InspectedElementContext.js @@ -134,7 +134,7 @@ function InspectedElementContextController({ children }: Props) { const value = hydrateHelper(data.value, data.path); const inspectedElement = { ...currentlyInspectedElement }; - fillInPath(inspectedElement, data.path, value); + fillInPath(inspectedElement, data.value, data.path, value); resource.write(element, inspectedElement); @@ -289,7 +289,7 @@ function hydrateHelper( path?: Array ): Object | null { if (dehydratedData !== null) { - let { cleaned, data } = dehydratedData; + let { cleaned, data, unserializable } = dehydratedData; if (path) { const { length } = path; @@ -297,10 +297,13 @@ function hydrateHelper( // 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)); + unserializable = unserializable.map(unserializablePath => + unserializablePath.slice(length) + ); } } - return hydrate(data, cleaned); + return hydrate(data, cleaned, unserializable); } else { return null; } diff --git a/src/devtools/views/Components/KeyValue.js b/src/devtools/views/Components/KeyValue.js index 1e1fb0b081..aef1e1db4f 100644 --- a/src/devtools/views/Components/KeyValue.js +++ b/src/devtools/views/Components/KeyValue.js @@ -16,6 +16,7 @@ type KeyValueProps = {| depth: number, hidden?: boolean, inspectPath?: InspectPath, + isReadOnly?: boolean, name: string, overrideValueFn?: ?OverrideValueFn, path: Array, @@ -25,6 +26,7 @@ type KeyValueProps = {| export default function KeyValue({ depth, inspectPath, + isReadOnly, hidden, name, overrideValueFn, @@ -78,17 +80,18 @@ export default function KeyValue({ displayValue = 'undefined'; } - const nameClassName = - typeof overrideValueFn === 'function' ? styles.EditableName : styles.Name; + const isEditable = typeof overrideValueFn === 'function' && !isReadOnly; children = (