vue3-yuanma/packages/runtime-core/__tests__/componentPublicInstance.spec.ts
2022-05-12 19:10:00 -04:00

462 lines
11 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 jest spying on proxy methods with Object.defineProperty', () => {
// #5417
let instanceProxy: any
const Comp = {
render() {},
setup() {
return {
toggle() {
return 'a'
}
}
},
mounted() {
instanceProxy = this
}
}
const app = createApp(Comp)
app.mount(nodeOps.createElement('div'))
// access 'toggle' to ensure key is cached
const v1 = instanceProxy.toggle()
expect(v1).toEqual('a')
// reconfigure "toggle" to be getter based.
let getCalledTimes = 0
Object.defineProperty(instanceProxy, 'toggle', {
get() {
getCalledTimes++
return () => 'b'
}
})
// getter should not be evaluated on initial definition
expect(getCalledTimes).toEqual(0)
// invoke "toggle" after "defineProperty"
const v2 = instanceProxy.toggle()
expect(v2).toEqual('b')
expect(getCalledTimes).toEqual(1)
// expect toggle getter not to be cached. it can't be
instanceProxy.toggle()
expect(getCalledTimes).toEqual(2)
// attaching jest spy, triggers the getter once, cache it and override the property.
// also uses Object.defineProperty
const spy = jest.spyOn(instanceProxy, 'toggle')
expect(getCalledTimes).toEqual(3)
// expect getter to not evaluate the jest spy caches its value
const v3 = instanceProxy.toggle()
expect(v3).toEqual('b')
expect(spy).toHaveBeenCalled()
expect(getCalledTimes).toEqual(3)
})
test('defineProperty on proxy property with value descriptor', () => {
// #5417
let instanceProxy: any
const Comp = {
render() {},
setup() {
return {
toggle: 'a'
}
},
mounted() {
instanceProxy = this
}
}
const app = createApp(Comp)
app.mount(nodeOps.createElement('div'))
const v1 = instanceProxy.toggle
expect(v1).toEqual('a')
Object.defineProperty(instanceProxy, 'toggle', {
value: 'b'
})
const v2 = instanceProxy.toggle
expect(v2).toEqual('b')
// expect null to be a settable value
Object.defineProperty(instanceProxy, 'toggle', {
value: null
})
const v3 = instanceProxy.toggle
expect(v3).toBeNull()
})
test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
// #5417
let instanceProxy: any
const Comp = {
props: ['fromProp'],
data() {
return { name: 'data.name' }
},
computed: {
greet() {
return 'Hi ' + (this as any).name
}
},
render() {},
setup() {
return {
fromSetup: true
}
},
mounted() {
instanceProxy = this
}
}
const app = createApp(Comp, {
fromProp: true
})
app.mount(nodeOps.createElement('div'))
expect(instanceProxy.greet).toEqual('Hi data.name')
// define property on data
Object.defineProperty(instanceProxy, 'name', {
get() {
return 'getter.name'
}
})
// computed is same still cached
expect(instanceProxy.greet).toEqual('Hi data.name')
// trigger computed
instanceProxy.name = ''
// expect "greet" to evaluated and use name from context getter
expect(instanceProxy.greet).toEqual('Hi getter.name')
// defineProperty on computed ( context )
Object.defineProperty(instanceProxy, 'greet', {
get() {
return 'Hi greet.getter.computed'
}
})
expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
// defineProperty on setupState
expect(instanceProxy.fromSetup).toBe(true)
Object.defineProperty(instanceProxy, 'fromSetup', {
get() {
return false
}
})
expect(instanceProxy.fromSetup).toBe(false)
// defineProperty on Props
expect(instanceProxy.fromProp).toBe(true)
Object.defineProperty(instanceProxy, 'fromProp', {
get() {
return false
}
})
expect(instanceProxy.fromProp).toBe(false)
})
// #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()
})
})