From 8457d8b980674b09547edb2dae28091306fe6aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Exbrayat?= Date: Sat, 12 Feb 2022 09:35:05 +0100 Subject: [PATCH] fix(runtime-core): allow spying on proxy methods (#4216) Since Jest v26.6.1, the mock method changed (see this commit https://github.com/facebook/jest/commit/30e802036291f4c9c9fd4feef6faba485df54dd2) to rely on `Object.defineProperty` in some cases. This breaks spying on proxy's methods, because even if Jest is properly calling `Object.defineProperty`, the cached value in the `get` section of the proxy is never updated, and the spy is in fact never used. This is easily reproducible as vue-next already uses a version of jest with these changes. This is blocking projects (like vue-test-utils-next and vue-cli) to update to recent Jest versions. This commit adds a `defineProperty` method to the proxy handler, that properly updates the defined value in the cache. --- .../__tests__/componentPublicInstance.spec.ts | 68 +++++++++++++++++++ .../src/componentPublicInstance.ts | 15 ++++ 2 files changed, 83 insertions(+) diff --git a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts index 0274351f863..fe588115d31 100644 --- a/packages/runtime-core/__tests__/componentPublicInstance.spec.ts +++ b/packages/runtime-core/__tests__/componentPublicInstance.spec.ts @@ -214,6 +214,74 @@ describe('component: proxy', () => { ]) }) + test('allow updating proxy with Object.defineProperty', () => { + let instanceProxy: any + const Comp = { + render() {}, + setup() { + return { + isDisplayed: true + } + }, + mounted() { + instanceProxy = this + } + } + + const app = createApp(Comp) + + app.mount(nodeOps.createElement('div')) + + Object.defineProperty(instanceProxy, 'isDisplayed', { value: false }) + + expect(instanceProxy.isDisplayed).toBe(false) + + Object.defineProperty(instanceProxy, 'isDisplayed', { value: true }) + + expect(instanceProxy.isDisplayed).toBe(true) + + Object.defineProperty(instanceProxy, 'isDisplayed', { + get() { + return false + } + }) + + expect(instanceProxy.isDisplayed).toBe(false) + + Object.defineProperty(instanceProxy, 'isDisplayed', { + get() { + return true + } + }) + + expect(instanceProxy.isDisplayed).toBe(true) + }) + + test('allow spying on proxy methods', () => { + let instanceProxy: any + const Comp = { + render() {}, + setup() { + return { + toggle() {} + } + }, + mounted() { + instanceProxy = this + } + } + + const app = createApp(Comp) + + app.mount(nodeOps.createElement('div')) + + const spy = jest.spyOn(instanceProxy, 'toggle') + + instanceProxy.toggle() + + expect(spy).toHaveBeenCalled() + }) + // #864 test('should not warn declared but absent props', () => { const Comp = { diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 1d7cfbdec69..0413730032c 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -397,8 +397,10 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { const { data, setupState, ctx } = instance if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value + return true } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { data[key] = value + return true } else if (hasOwn(instance.props, key)) { __DEV__ && warn( @@ -445,6 +447,19 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { hasOwn(publicPropertiesMap, key) || hasOwn(appContext.config.globalProperties, key) ) + }, + + defineProperty( + target: ComponentRenderContext, + key: string, + descriptor: PropertyDescriptor + ) { + if (descriptor.get != null) { + this.set!(target, key, descriptor.get(), null) + } else if (descriptor.value != null) { + this.set!(target, key, descriptor.value, null) + } + return Reflect.defineProperty(target, key, descriptor) } }