8457d8b980
Since Jest v26.6.1, the mock method changed (see this commit 30e8020362
) 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.
319 lines
7.6 KiB
TypeScript
319 lines
7.6 KiB
TypeScript
import {
|
|
h,
|
|
render,
|
|
getCurrentInstance,
|
|
nodeOps,
|
|
createApp,
|
|
shallowReadonly
|
|
} from '@vue/runtime-test'
|
|
import { ComponentInternalInstance, ComponentOptions } from '../src/component'
|
|
|
|
describe('component: proxy', () => {
|
|
test('data', () => {
|
|
let instance: ComponentInternalInstance
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
data() {
|
|
return {
|
|
foo: 1
|
|
}
|
|
},
|
|
mounted() {
|
|
instance = getCurrentInstance()!
|
|
instanceProxy = this
|
|
},
|
|
render() {
|
|
return null
|
|
}
|
|
}
|
|
render(h(Comp), nodeOps.createElement('div'))
|
|
expect(instanceProxy.foo).toBe(1)
|
|
instanceProxy.foo = 2
|
|
expect(instance!.data.foo).toBe(2)
|
|
})
|
|
|
|
test('setupState', () => {
|
|
let instance: ComponentInternalInstance
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
setup() {
|
|
return {
|
|
foo: 1
|
|
}
|
|
},
|
|
mounted() {
|
|
instance = getCurrentInstance()!
|
|
instanceProxy = this
|
|
},
|
|
render() {
|
|
return null
|
|
}
|
|
}
|
|
render(h(Comp), nodeOps.createElement('div'))
|
|
expect(instanceProxy.foo).toBe(1)
|
|
instanceProxy.foo = 2
|
|
expect(instance!.setupState.foo).toBe(2)
|
|
})
|
|
|
|
test('should not expose non-declared props', () => {
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
setup() {
|
|
return () => null
|
|
},
|
|
mounted() {
|
|
instanceProxy = this
|
|
}
|
|
}
|
|
render(h(Comp, { count: 1 }), nodeOps.createElement('div'))
|
|
expect('count' in instanceProxy).toBe(false)
|
|
})
|
|
|
|
test('public properties', async () => {
|
|
let instance: ComponentInternalInstance
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
setup() {
|
|
return () => null
|
|
},
|
|
mounted() {
|
|
instance = getCurrentInstance()!
|
|
instanceProxy = this
|
|
}
|
|
}
|
|
render(h(Comp), nodeOps.createElement('div'))
|
|
expect(instanceProxy.$data).toBe(instance!.data)
|
|
expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props))
|
|
expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs))
|
|
expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots))
|
|
expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs))
|
|
expect(instanceProxy.$parent).toBe(
|
|
instance!.parent && instance!.parent.proxy
|
|
)
|
|
expect(instanceProxy.$root).toBe(instance!.root.proxy)
|
|
expect(instanceProxy.$emit).toBe(instance!.emit)
|
|
expect(instanceProxy.$el).toBe(instance!.vnode.el)
|
|
expect(instanceProxy.$options).toBe(instance!.type as ComponentOptions)
|
|
expect(() => (instanceProxy.$data = {})).toThrow(TypeError)
|
|
expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
|
|
|
|
const nextTickThis = await instanceProxy.$nextTick(function (this: any) {
|
|
return this
|
|
})
|
|
expect(nextTickThis).toBe(instanceProxy)
|
|
})
|
|
|
|
test('user attached properties', async () => {
|
|
let instance: ComponentInternalInstance
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
setup() {
|
|
return () => null
|
|
},
|
|
mounted() {
|
|
instance = getCurrentInstance()!
|
|
instanceProxy = this
|
|
}
|
|
}
|
|
render(h(Comp), nodeOps.createElement('div'))
|
|
instanceProxy.foo = 1
|
|
expect(instanceProxy.foo).toBe(1)
|
|
expect(instance!.ctx.foo).toBe(1)
|
|
|
|
// should also allow properties that start with $
|
|
const obj = (instanceProxy.$store = {})
|
|
expect(instanceProxy.$store).toBe(obj)
|
|
expect(instance!.ctx.$store).toBe(obj)
|
|
})
|
|
|
|
test('globalProperties', () => {
|
|
let instance: ComponentInternalInstance
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
setup() {
|
|
return () => null
|
|
},
|
|
mounted() {
|
|
instance = getCurrentInstance()!
|
|
instanceProxy = this
|
|
}
|
|
}
|
|
|
|
const app = createApp(Comp)
|
|
app.config.globalProperties.foo = 1
|
|
app.mount(nodeOps.createElement('div'))
|
|
|
|
expect(instanceProxy.foo).toBe(1)
|
|
|
|
// set should overwrite globalProperties with local
|
|
instanceProxy.foo = 2
|
|
// expect(instanceProxy.foo).toBe(2)
|
|
expect(instance!.ctx.foo).toBe(2)
|
|
// should not affect global
|
|
expect(app.config.globalProperties.foo).toBe(1)
|
|
})
|
|
|
|
test('has check', () => {
|
|
let instanceProxy: any
|
|
const Comp = {
|
|
render() {},
|
|
props: {
|
|
msg: String
|
|
},
|
|
data() {
|
|
return {
|
|
foo: 0
|
|
}
|
|
},
|
|
setup() {
|
|
return {
|
|
bar: 1
|
|
}
|
|
},
|
|
mounted() {
|
|
instanceProxy = this
|
|
}
|
|
}
|
|
|
|
const app = createApp(Comp, { msg: 'hello' })
|
|
app.config.globalProperties.global = 1
|
|
|
|
app.mount(nodeOps.createElement('div'))
|
|
|
|
// props
|
|
expect('msg' in instanceProxy).toBe(true)
|
|
// data
|
|
expect('foo' in instanceProxy).toBe(true)
|
|
// ctx
|
|
expect('bar' in instanceProxy).toBe(true)
|
|
// public properties
|
|
expect('$el' in instanceProxy).toBe(true)
|
|
// global properties
|
|
expect('global' in instanceProxy).toBe(true)
|
|
|
|
// non-existent
|
|
expect('$foobar' in instanceProxy).toBe(false)
|
|
expect('baz' in instanceProxy).toBe(false)
|
|
|
|
// #4962 triggering getter should not cause non-existent property to
|
|
// pass the has check
|
|
instanceProxy.baz
|
|
expect('baz' in instanceProxy).toBe(false)
|
|
|
|
// set non-existent (goes into proxyTarget sink)
|
|
instanceProxy.baz = 1
|
|
expect('baz' in instanceProxy).toBe(true)
|
|
|
|
// dev mode ownKeys check for console inspection
|
|
// should only expose own keys
|
|
expect(Object.keys(instanceProxy)).toMatchObject([
|
|
'msg',
|
|
'bar',
|
|
'foo',
|
|
'baz'
|
|
])
|
|
})
|
|
|
|
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 = {
|
|
props: ['test'],
|
|
render(this: any) {
|
|
return this.test
|
|
}
|
|
}
|
|
render(h(Comp), nodeOps.createElement('div'))
|
|
expect(
|
|
`was accessed during render but is not defined`
|
|
).not.toHaveBeenWarned()
|
|
})
|
|
|
|
test('should allow symbol to access on render', () => {
|
|
const Comp = {
|
|
render() {
|
|
if ((this as any)[Symbol.unscopables]) {
|
|
return '1'
|
|
}
|
|
return '2'
|
|
}
|
|
}
|
|
|
|
const app = createApp(Comp)
|
|
app.mount(nodeOps.createElement('div'))
|
|
|
|
expect(
|
|
`Property ${JSON.stringify(
|
|
Symbol.unscopables
|
|
)} was accessed during render ` + `but is not defined on instance.`
|
|
).toHaveBeenWarned()
|
|
})
|
|
})
|