diff --git a/packages/devtools-kit/__tests__/component/replacer.test.ts b/packages/devtools-kit/__tests__/component/replacer.test.ts new file mode 100644 index 000000000..69782d358 --- /dev/null +++ b/packages/devtools-kit/__tests__/component/replacer.test.ts @@ -0,0 +1,49 @@ +import { stringifyReplacer } from '../../src/core/component/state/replacer' + +// `stringifyReplacer` reads the value from `this[key]`, mirroring how it is +// called during `JSON.stringify`. +function replace(value: unknown) { + return stringifyReplacer.call({ value }, 'value') as { _custom?: { type?: string, displayText?: string } } +} + +describe('stringifyReplacer: component definition detection', () => { + it('does not treat a plain class instance with a render method as a component', () => { + class Chart { + data = [1, 2, 3] + render() { + return 'draw the chart' + } + } + + const result = replace(new Chart()) + + expect(result?._custom?.type).not.toBe('component-definition') + }) + + it('still detects a real Vue component definition', () => { + const Button = defineComponent({ + name: 'MyButton', + render() { + return h('button', 'Click me') + }, + }) + + const result = replace(Button) + + expect(result?._custom?.type).toBe('component-definition') + expect(result?._custom?.displayText).toBe('MyButton') + }) + + it('does not treat a reactive object with a render method as a component', () => { + const store = reactive({ + data: [1, 2, 3], + render() { + return 'draw the chart' + }, + }) + + const result = replace(store) + + expect(result?._custom?.type).not.toBe('component-definition') + }) +}) diff --git a/packages/devtools-kit/src/core/component/state/replacer.ts b/packages/devtools-kit/src/core/component/state/replacer.ts index f711abf9d..cc4a261b4 100644 --- a/packages/devtools-kit/src/core/component/state/replacer.ts +++ b/packages/devtools-kit/src/core/component/state/replacer.ts @@ -1,7 +1,7 @@ import { ensurePropertyExists } from '../utils' import { INFINITY, MAX_ARRAY_SIZE, MAX_STRING_SIZE, NAN, NEGATIVE_INFINITY, UNDEFINED } from './constants' import { getBigIntDetails, getComponentDefinitionDetails, getDateDetails, getFunctionDetails, getHTMLElementDetails, getInstanceDetails, getMapDetails, getObjectDetails, getSetDetails, getStoreDetails } from './custom' -import { isVueInstance } from './is' +import { isReactive, isVueInstance } from './is' import { sanitize } from './util' export type Replacer = (this: any, key: string | number, value: any, depth?: number, seenInstance?: Map) => any @@ -82,7 +82,7 @@ export function stringifyReplacer(key: string | number, _value: any, depth?: num seenInstance?.set(val, depth!) return componentVal } - else if (ensurePropertyExists(val, 'render', true) && typeof val.render === 'function') { + else if (Object.prototype.hasOwnProperty.call(val, 'render') && typeof (val as Record).render === 'function' && !isReactive(val)) { return getComponentDefinitionDetails(val) } else if (val.constructor && val.constructor.name === 'VNode') {