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;
+}