diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts index 3a9532363cee..e41e885f556d 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/object-utils.ts @@ -45,4 +45,4 @@ export function getKeys(obj: {}): string[] { */ export const getDescriptor = (instance: any, propName: string): PropertyDescriptor | undefined => Object.getOwnPropertyDescriptor(instance, propName) || - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), propName); + Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance) ?? {}, propName); diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts index 40bf8a47020d..99c8d4b53c85 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/serialized-descriptor-factory.ts @@ -50,6 +50,14 @@ const serializable: Set = new Set([ PropType.Unknown, ]); +const getConstructorName = (prop: object, fallback: string): string => { + const constructorName = (prop as {constructor?: {name?: unknown}}).constructor?.name; + + return typeof constructorName === 'string' && constructorName.length > 0 + ? constructorName + : fallback; +}; + const typeToDescriptorPreview: Formatter = { [PropType.Array]: (prop: Array) => `Array(${prop.length})`, [PropType.Set]: (prop: Set) => `Set(${prop.size})`, @@ -58,12 +66,16 @@ const typeToDescriptorPreview: Formatter = { [PropType.Boolean]: (prop: boolean) => truncate(prop.toString()), [PropType.String]: (prop: string) => `"${prop}"`, [PropType.Function]: (prop: Function) => `${prop.name ? 'ƒ ' : ''}(...)`, - [PropType.HTMLNode]: (prop: Node) => prop.constructor.name, + [PropType.HTMLNode]: (prop: Node) => getConstructorName(prop, 'Node'), [PropType.Null]: (_: null) => 'null', [PropType.Number]: (prop: any) => prop.toString(), - [PropType.Object]: (prop: Object) => - (prop.constructor.name !== 'Object' ? `${prop.constructor.name} ` : '') + - (getKeys(prop).length > 0 ? '{...}' : '{}'), + [PropType.Object]: (prop: object) => { + const constructorName = getConstructorName(prop, 'Object'); + return ( + (constructorName !== 'Object' ? `${constructorName} ` : '') + + (getKeys(prop).length > 0 ? '{...}' : '{}') + ); + }, [PropType.Symbol]: (symbol: symbol) => `Symbol(${symbol.description})`, [PropType.Undefined]: (_: undefined) => 'undefined', [PropType.Date]: (prop: unknown) => { @@ -308,7 +320,10 @@ function getNestedDescriptorValue( case PropType.Object: return nodes.reduce( (accumulator, nestedProp) => { - if (prop.hasOwnProperty(nestedProp.name) && !ignoreList.has(nestedProp.name)) { + if ( + Object.prototype.hasOwnProperty.call(value, nestedProp.name) && + !ignoreList.has(nestedProp.name) + ) { accumulator[nestedProp.name] = nestedSerializer( value, nestedProp.name, diff --git a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts index ec59e2549555..47b3b18b826b 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/state-serializer/state-serializer.spec.ts @@ -9,7 +9,7 @@ import {PropType} from '../../../../protocol'; import {getDescriptor, getKeys} from './object-utils'; -import {deeplySerializeSelectedProperties} from './state-serializer'; +import {deeplySerializeSelectedProperties, serializeDirectiveState} from './state-serializer'; const QUERY_1_1: any[] = []; @@ -494,6 +494,50 @@ describe('deeplySerializeSelectedProperties', () => { }); }); + it('should preview objects without prototypes as plain objects', () => { + const grouped = Object.create(null); + grouped.foo = 1; + + const result = serializeDirectiveState({grouped}); + + expect(result['grouped']).toEqual({ + type: PropType.Object, + editable: false, + expandable: true, + preview: '{...}', + containerType: null, + }); + }); + + it('should deeply serialize selected properties from objects without prototypes', () => { + const grouped = Object.create(null); + grouped.foo = 1; + + const result = deeplySerializeSelectedProperties({grouped}, [ + {name: 'grouped', children: [{name: 'foo', children: []}]}, + ]); + + expect(result).toEqual({ + grouped: { + type: PropType.Object, + editable: false, + expandable: true, + preview: '{...}', + value: { + foo: { + type: PropType.Number, + expandable: false, + editable: true, + preview: '1', + value: 1, + containerType: null, + }, + }, + containerType: null, + }, + }); + }); + it('getDescriptor should get the descriptors for both getters and setters correctly from the prototype', () => { const instance = { __proto__: { @@ -555,6 +599,12 @@ describe('deeplySerializeSelectedProperties', () => { expect(getKeys(instance)).toEqual([]); }); + it('getDescriptor should not throw on object without prototype', () => { + const instance = Object.create(null); + + expect(getDescriptor(instance, 'foo')).toBeUndefined(); + }); + it('getKeys would ignore getters and setters for "__proto__"', () => { const instance = { baz: 2,