diff --git a/fixtures/devtools/standalone/index.html b/fixtures/devtools/standalone/index.html index 28255cb67ee6c..235ceeb16ed76 100644 --- a/fixtures/devtools/standalone/index.html +++ b/fixtures/devtools/standalone/index.html @@ -208,6 +208,70 @@

List

return ; } + const baseInheritedKeys = Object.create(Object.prototype, { + enumerableStringBase: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('enumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableStringBase: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('nonEnumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + }); + + const inheritedKeys = Object.create(baseInheritedKeys, { + enumerableString: { + value: 2, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableString: { + value: 3, + writable: true, + enumerable: false, + configurable: true, + }, + 123: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('nonEnumerableSymbol')]: { + value: 2, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('enumerableSymbol')]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + }); + + function InheritedKeys() { + return ; + } + const object = { string: "abc", longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890", @@ -294,6 +358,7 @@

List

+ ); } diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap index 1989aee6391a9..f56288c12e640 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap @@ -541,6 +541,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected "object_of_objects": { "inner": {} }, + "object_with_symbol": { + "Symbol(name)": "hello" + }, "proxy": {}, "react_element": {}, "regexp": {}, @@ -612,6 +615,25 @@ exports[`InspectedElementContext should support objects with overridden hasOwnPr } `; +exports[`InspectedElementContext should support objects with with inherited keys: 1: Inspected element 2 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": null, + "props": { + "object": { + "123": 3, + "enumerableString": 2, + "Symbol(enumerableSymbol)": 3, + "enumerableStringBase": 1, + "Symbol(enumerableSymbolBase)": 1 + } + }, + "state": null +} +`; + exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = ` { "id": 2, diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js index 29e8e379e4e43..ae6fdfbc831d0 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js @@ -532,6 +532,9 @@ describe('InspectedElementContext', () => { const objectOfObjects = { inner: {string: 'abc', number: 123, boolean: true}, }; + const objectWithSymbol = { + [Symbol('name')]: 'hello', + }; const typedArray = Int8Array.from([100, -100, 0]); const arrayBuffer = typedArray.buffer; const dataView = new DataView(arrayBuffer); @@ -575,6 +578,7 @@ describe('InspectedElementContext', () => { map={mapShallow} map_of_maps={mapOfMaps} object_of_objects={objectOfObjects} + object_with_symbol={objectWithSymbol} proxy={proxyInstance} react_element={} regexp={/abc/giu} @@ -628,6 +632,7 @@ describe('InspectedElementContext', () => { map, map_of_maps, object_of_objects, + object_with_symbol, proxy, react_element, regexp, @@ -732,6 +737,8 @@ describe('InspectedElementContext', () => { ); expect(object_of_objects.inner[meta.preview_short]).toBe('{…}'); + expect(object_with_symbol['Symbol(name)']).toBe('hello'); + expect(proxy[meta.inspectable]).toBe(false); expect(proxy[meta.name]).toBe('function'); expect(proxy[meta.type]).toBe('function'); @@ -934,6 +941,111 @@ describe('InspectedElementContext', () => { done(); }); + it('should support objects with with inherited keys', async done => { + const Example = () => null; + + const base = Object.create(Object.prototype, { + enumerableStringBase: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('enumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableStringBase: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('nonEnumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + }); + + const object = Object.create(base, { + enumerableString: { + value: 2, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableString: { + value: 3, + writable: true, + enumerable: false, + configurable: true, + }, + [123]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('nonEnumerableSymbol')]: { + value: 2, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('enumerableSymbol')]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + }); + + const container = document.createElement('div'); + await utils.actAsync(() => + ReactDOM.render(, 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}`); + expect(inspectedElement.props.object).toEqual({ + 123: 3, + 'Symbol(enumerableSymbol)': 3, + 'Symbol(enumerableSymbolBase)': 1, + enumerableString: 2, + enumerableStringBase: 1, + }); + + done(); + }); + it('should not dehydrate nested values until explicitly requested', async done => { const Example = () => { const [state] = React.useState({ diff --git a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap index 18ff7483c3f5e..11eb16474363a 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap @@ -236,6 +236,29 @@ Object { } `; +exports[`InspectedElementContext should support objects with with inherited keys: 1: Initial inspection 1`] = ` +Object { + "id": 2, + "type": "full-data", + "value": { + "id": 2, + "owners": null, + "context": {}, + "hooks": null, + "props": { + "data": { + "123": 3, + "enumerableString": 2, + "Symbol(enumerableSymbol)": 3, + "enumerableStringBase": 1, + "Symbol(enumerableSymbolBase)": 1 + } + }, + "state": null +}, +} +`; + exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = ` Object { "id": 2, diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index 7f1c461d93542..18a965b24b56e 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -432,6 +432,81 @@ describe('InspectedElementContext', () => { done(); }); + it('should support objects with with inherited keys', async done => { + const Example = () => null; + + const base = Object.create(Object.prototype, { + enumerableStringBase: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('enumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableStringBase: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('nonEnumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + }); + + const object = Object.create(base, { + enumerableString: { + value: 2, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableString: { + value: 3, + writable: true, + enumerable: false, + configurable: true, + }, + [123]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('nonEnumerableSymbol')]: { + value: 2, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('enumerableSymbol')]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + }); + + act(() => + ReactDOM.render(, 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; diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index 2815be0f2d220..1f6c0d2af6efa 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -10,6 +10,7 @@ import { getDataType, getDisplayNameForReactElement, + getAllEnumerableKeys, getInObject, formatDataForPreview, setInObject, @@ -291,16 +292,17 @@ export function dehydrate( return createDehydrated(type, true, data, cleaned, path); } else { const object = {}; - for (const name in data) { + getAllEnumerableKeys(data).forEach(key => { + const name = key.toString(); object[name] = dehydrate( - data[name], + data[key], cleaned, unserializable, path.concat([name]), isPathAllowed, isPathAllowedCheck ? 1 : level + 1, ); - } + }); return object; } diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index b0b873cca163d..a0ec1038e75dc 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -52,16 +52,41 @@ const cachedDisplayNames: WeakMap = new WeakMap(); // Try to reuse the already encoded strings. const encodedStringCache = new LRU({max: 1000}); -export function alphaSortKeys(a: string, b: string): number { - if (a > b) { +export function alphaSortKeys( + a: string | number | Symbol, + b: string | number | Symbol, +): number { + if (a.toString() > b.toString()) { return 1; - } else if (b > a) { + } else if (b.toString() > a.toString()) { return -1; } else { return 0; } } +export function getAllEnumerableKeys( + obj: Object, +): Array { + const keys = []; + let current = obj; + while (current != null) { + const currentKeys = [ + ...Object.keys(current), + ...Object.getOwnPropertySymbols(current), + ]; + const descriptors = Object.getOwnPropertyDescriptors(current); + currentKeys.forEach(key => { + // $FlowFixMe: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor + if (descriptors[key].enumerable) { + keys.push(key); + } + }); + current = Object.getPrototypeOf(current); + } + return keys; +} + export function getDisplayName( type: Function, fallbackName: string = 'Anonymous', @@ -657,7 +682,7 @@ export function formatDataForPreview( return data.toString(); case 'object': if (showFormattedValue) { - const keys = Object.keys(data).sort(alphaSortKeys); + const keys = getAllEnumerableKeys(data).sort(alphaSortKeys); let formatted = ''; for (let i = 0; i < keys.length; i++) { @@ -665,7 +690,10 @@ export function formatDataForPreview( if (i > 0) { formatted += ', '; } - formatted += `${key}: ${formatDataForPreview(data[key], false)}`; + formatted += `${key.toString()}: ${formatDataForPreview( + data[key], + false, + )}`; if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { // Prevent doing a lot of unnecessary iteration... break; diff --git a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js index 49fc1c4f2edd6..1403ece9f6dbf 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js @@ -17,6 +17,7 @@ import CustomObject from './CustomObject'; import EdgeCaseObjects from './EdgeCaseObjects.js'; import NestedProps from './NestedProps'; import SimpleValues from './SimpleValues'; +import SymbolKeys from './SymbolKeys'; // TODO Add Immutable JS example @@ -32,6 +33,7 @@ export default function InspectableElements() { + ); } diff --git a/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js b/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js new file mode 100644 index 0000000000000..3aae9c8837bc8 --- /dev/null +++ b/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +const base = Object.create(Object.prototype, { + enumerableStringBase: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('enumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableStringBase: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('nonEnumerableSymbolBase')]: { + value: 1, + writable: true, + enumerable: false, + configurable: true, + }, +}); + +const data = Object.create(base, { + enumerableString: { + value: 2, + writable: true, + enumerable: true, + configurable: true, + }, + nonEnumerableString: { + value: 3, + writable: true, + enumerable: false, + configurable: true, + }, + [123]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, + [Symbol('nonEnumerableSymbol')]: { + value: 2, + writable: true, + enumerable: false, + configurable: true, + }, + [Symbol('enumerableSymbol')]: { + value: 3, + writable: true, + enumerable: true, + configurable: true, + }, +}); + +export default function SymbolKeys() { + return ; +} + +function ChildComponent(props: any) { + return null; +}