Skip to content

Commit 1574edd

Browse files
authored
fix(runtime-core): allow spying on proxy methods regression (#5417)
fix #5415 (regression by #4216)
1 parent cea82cf commit 1574edd

File tree

2 files changed

+151
-5
lines changed

2 files changed

+151
-5
lines changed

packages/runtime-core/__tests__/componentPublicInstance.spec.ts

+148-3
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,17 @@ describe('component: proxy', () => {
257257
expect(instanceProxy.isDisplayed).toBe(true)
258258
})
259259

260-
test('allow spying on proxy methods', () => {
260+
261+
test('allow jest spying on proxy methods with Object.defineProperty', () => {
262+
// #5417
261263
let instanceProxy: any
262264
const Comp = {
263265
render() {},
264266
setup() {
265267
return {
266-
toggle() {}
268+
toggle() {
269+
return 'a'
270+
}
267271
}
268272
},
269273
mounted() {
@@ -275,13 +279,154 @@ describe('component: proxy', () => {
275279

276280
app.mount(nodeOps.createElement('div'))
277281

278-
const spy = jest.spyOn(instanceProxy, 'toggle')
282+
// access 'toggle' to ensure key is cached
283+
const v1 = instanceProxy.toggle()
284+
expect(v1).toEqual('a')
285+
286+
// reconfigure "toggle" to be getter based.
287+
let getCalledTimes = 0
288+
Object.defineProperty(instanceProxy, 'toggle', {
289+
get() {
290+
getCalledTimes++
291+
return () => 'b'
292+
}
293+
})
279294

295+
// getter should not be evaluated on initial definition
296+
expect(getCalledTimes).toEqual(0)
297+
298+
// invoke "toggle" after "defineProperty"
299+
const v2 = instanceProxy.toggle()
300+
expect(v2).toEqual('b')
301+
expect(getCalledTimes).toEqual(1)
302+
303+
// expect toggle getter not to be cached. it can't be
280304
instanceProxy.toggle()
305+
expect(getCalledTimes).toEqual(2)
281306

307+
// attaching jest spy, triggers the getter once, cache it and override the property.
308+
// also uses Object.defineProperty
309+
const spy = jest.spyOn(instanceProxy, 'toggle')
310+
expect(getCalledTimes).toEqual(3)
311+
312+
// expect getter to not evaluate the jest spy caches its value
313+
const v3 = instanceProxy.toggle()
314+
expect(v3).toEqual('b')
282315
expect(spy).toHaveBeenCalled()
316+
expect(getCalledTimes).toEqual(3)
317+
})
318+
319+
test('defineProperty on proxy property with value descriptor', () => {
320+
// #5417
321+
let instanceProxy: any
322+
const Comp = {
323+
render() {},
324+
setup() {
325+
return {
326+
toggle: 'a'
327+
}
328+
},
329+
mounted() {
330+
instanceProxy = this
331+
}
332+
}
333+
334+
const app = createApp(Comp)
335+
336+
app.mount(nodeOps.createElement('div'))
337+
338+
const v1 = instanceProxy.toggle
339+
expect(v1).toEqual('a')
340+
341+
Object.defineProperty(instanceProxy, 'toggle', {
342+
value: 'b'
343+
})
344+
const v2 = instanceProxy.toggle
345+
expect(v2).toEqual('b')
346+
347+
// expect null to be a settable value
348+
Object.defineProperty(instanceProxy, 'toggle', {
349+
value: null
350+
})
351+
const v3 = instanceProxy.toggle
352+
expect(v3).toBeNull()
353+
})
354+
355+
test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
356+
// #5417
357+
let instanceProxy: any
358+
const Comp = {
359+
props: ['fromProp'],
360+
data() {
361+
return { name: 'data.name' }
362+
},
363+
computed: {
364+
greet() {
365+
return 'Hi ' + (this as any).name
366+
}
367+
},
368+
render() {},
369+
setup() {
370+
return {
371+
fromSetup: true
372+
}
373+
},
374+
mounted() {
375+
instanceProxy = this
376+
}
377+
}
378+
379+
const app = createApp(Comp, {
380+
fromProp: true
381+
})
382+
383+
app.mount(nodeOps.createElement('div'))
384+
expect(instanceProxy.greet).toEqual('Hi data.name')
385+
386+
// define property on data
387+
Object.defineProperty(instanceProxy, 'name', {
388+
get() {
389+
return 'getter.name'
390+
}
391+
})
392+
393+
// computed is same still cached
394+
expect(instanceProxy.greet).toEqual('Hi data.name')
395+
396+
// trigger computed
397+
instanceProxy.name = ''
398+
399+
// expect "greet" to evaluated and use name from context getter
400+
expect(instanceProxy.greet).toEqual('Hi getter.name')
401+
402+
// defineProperty on computed ( context )
403+
Object.defineProperty(instanceProxy, 'greet', {
404+
get() {
405+
return 'Hi greet.getter.computed'
406+
}
407+
})
408+
expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
409+
410+
// defineProperty on setupState
411+
expect(instanceProxy.fromSetup).toBe(true)
412+
Object.defineProperty(instanceProxy, 'fromSetup', {
413+
get() {
414+
return false
415+
}
416+
})
417+
expect(instanceProxy.fromSetup).toBe(false)
418+
419+
// defineProperty on Props
420+
expect(instanceProxy.fromProp).toBe(true)
421+
Object.defineProperty(instanceProxy, 'fromProp', {
422+
get() {
423+
return false
424+
}
425+
})
426+
expect(instanceProxy.fromProp).toBe(false)
283427
})
284428

429+
285430
// #864
286431
test('should not warn declared but absent props', () => {
287432
const Comp = {

packages/runtime-core/src/componentPublicInstance.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,9 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
455455
descriptor: PropertyDescriptor
456456
) {
457457
if (descriptor.get != null) {
458-
this.set!(target, key, descriptor.get(), null)
459-
} else if (descriptor.value != null) {
458+
// invalidate key cache of a getter based property #5417
459+
target.$.accessCache[key] = 0;
460+
} else if (hasOwn(descriptor,'value')) {
460461
this.set!(target, key, descriptor.value, null)
461462
}
462463
return Reflect.defineProperty(target, key, descriptor)

0 commit comments

Comments
 (0)