mirror of https://github.com/vuejs/core.git
test(runtime-vapor): component props (#99)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
8dec243dc1
commit
ea5f7ec076
|
@ -711,7 +711,7 @@ describe('component props', () => {
|
|||
)
|
||||
})
|
||||
|
||||
// #691ef
|
||||
// #6915
|
||||
test('should not mutate original props long-form definition object', () => {
|
||||
const props = {
|
||||
msg: {
|
||||
|
|
|
@ -0,0 +1,487 @@
|
|||
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
|
||||
|
||||
// NOTE: not supported
|
||||
// mixins
|
||||
// caching
|
||||
|
||||
import { type FunctionalComponent, setCurrentInstance } from '../src/component'
|
||||
import {
|
||||
children,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
ref,
|
||||
render,
|
||||
setText,
|
||||
template,
|
||||
watchEffect,
|
||||
} from '../src'
|
||||
|
||||
let host: HTMLElement
|
||||
const initHost = () => {
|
||||
host = document.createElement('div')
|
||||
host.setAttribute('id', 'host')
|
||||
document.body.appendChild(host)
|
||||
}
|
||||
beforeEach(() => initHost())
|
||||
afterEach(() => host.remove())
|
||||
|
||||
describe('component props (vapor)', () => {
|
||||
test('stateful', () => {
|
||||
let props: any
|
||||
// TODO: attrs
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: ['fooBar', 'barBaz'],
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
props = instance.props
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get fooBar() {
|
||||
return 1
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.fooBar).toEqual(1)
|
||||
|
||||
// test passing kebab-case and resolving to camelCase
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get ['foo-bar']() {
|
||||
return 2
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.fooBar).toEqual(2)
|
||||
|
||||
// test updating kebab-case should not delete it (#955)
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get ['foo-bar']() {
|
||||
return 3
|
||||
},
|
||||
get barBaz() {
|
||||
return 5
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.fooBar).toEqual(3)
|
||||
expect(props.barBaz).toEqual(5)
|
||||
|
||||
render(Comp, {}, host)
|
||||
expect(props.fooBar).toBeUndefined()
|
||||
expect(props.barBaz).toBeUndefined()
|
||||
// expect(props.qux).toEqual(5) // TODO: attrs
|
||||
})
|
||||
|
||||
test.todo('stateful with setup', () => {
|
||||
// TODO:
|
||||
})
|
||||
|
||||
test('functional with declaration', () => {
|
||||
let props: any
|
||||
// TODO: attrs
|
||||
|
||||
const Comp: FunctionalComponent = defineComponent((_props: any) => {
|
||||
const instance = getCurrentInstance()!
|
||||
props = instance.props
|
||||
return {}
|
||||
})
|
||||
Comp.props = ['foo']
|
||||
Comp.render = (() => {}) as any
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 1
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toEqual(1)
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toEqual(2)
|
||||
|
||||
render(Comp, {}, host)
|
||||
expect(props.foo).toBeUndefined()
|
||||
})
|
||||
|
||||
test('functional without declaration', () => {
|
||||
let props: any
|
||||
// TODO: attrs
|
||||
|
||||
const Comp: FunctionalComponent = defineComponent((_props: any) => {
|
||||
const instance = getCurrentInstance()!
|
||||
props = instance.props
|
||||
return {}
|
||||
})
|
||||
Comp.props = undefined as any
|
||||
Comp.render = (() => {}) as any
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 1
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBeUndefined()
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBeUndefined()
|
||||
})
|
||||
|
||||
test('boolean casting', () => {
|
||||
let props: any
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
foo: Boolean,
|
||||
bar: Boolean,
|
||||
baz: Boolean,
|
||||
qux: Boolean,
|
||||
},
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
props = instance.props
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
// absent should cast to false
|
||||
bar: '', // empty string should cast to true
|
||||
baz: 'baz', // same string should cast to true
|
||||
qux: 'ok', // other values should be left in-tact (but raise warning)
|
||||
},
|
||||
host,
|
||||
)
|
||||
|
||||
expect(props.foo).toBe(false)
|
||||
expect(props.bar).toBe(true)
|
||||
expect(props.baz).toBe(true)
|
||||
expect(props.qux).toBe('ok')
|
||||
})
|
||||
|
||||
test('default value', () => {
|
||||
let props: any
|
||||
const defaultFn = vi.fn(() => ({ a: 1 }))
|
||||
const defaultBaz = vi.fn(() => ({ b: 1 }))
|
||||
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
foo: {
|
||||
default: 1,
|
||||
},
|
||||
bar: {
|
||||
default: defaultFn,
|
||||
},
|
||||
baz: {
|
||||
type: Function,
|
||||
default: defaultBaz,
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
props = instance.props
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 2
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBe(2)
|
||||
// const prevBar = props.bar
|
||||
props.bar
|
||||
expect(props.bar).toEqual({ a: 1 })
|
||||
expect(props.baz).toEqual(defaultBaz)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported)
|
||||
expect(defaultFn).toHaveBeenCalledTimes(2)
|
||||
expect(defaultBaz).toHaveBeenCalledTimes(0)
|
||||
|
||||
// #999: updates should not cause default factory of unchanged prop to be
|
||||
// called again
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 3
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBe(3)
|
||||
expect(props.bar).toEqual({ a: 1 })
|
||||
// expect(props.bar).toBe(prevBar) // failed: (caching is not supported)
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get bar() {
|
||||
return { b: 2 }
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBe(1)
|
||||
expect(props.bar).toEqual({ b: 2 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return 3
|
||||
},
|
||||
get bar() {
|
||||
return { b: 3 }
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBe(3)
|
||||
expect(props.bar).toEqual({ b: 3 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get bar() {
|
||||
return { b: 4 }
|
||||
},
|
||||
},
|
||||
host,
|
||||
)
|
||||
expect(props.foo).toBe(1)
|
||||
expect(props.bar).toEqual({ b: 4 })
|
||||
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
|
||||
})
|
||||
|
||||
test.todo('using inject in default value factory', () => {
|
||||
// TODO: impl inject
|
||||
})
|
||||
|
||||
// NOTE: maybe it's unnecessary
|
||||
// https://github.com/vuejs/core-vapor/pull/99#discussion_r1472647377
|
||||
test('optimized props updates', async () => {
|
||||
const Child = defineComponent({
|
||||
props: ['foo'],
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
const t0 = template('<div><!></div>')
|
||||
const n0 = t0()
|
||||
const {
|
||||
0: [n1],
|
||||
} = children(n0)
|
||||
watchEffect(() => {
|
||||
setText(n1, instance.props.foo)
|
||||
})
|
||||
return n0
|
||||
},
|
||||
})
|
||||
|
||||
const foo = ref(1)
|
||||
const id = ref('a')
|
||||
const Comp = defineComponent({
|
||||
setup() {
|
||||
return { foo, id }
|
||||
},
|
||||
render(_ctx: Record<string, any>) {
|
||||
const t0 = template('')
|
||||
const n0 = t0()
|
||||
render(
|
||||
Child,
|
||||
{
|
||||
get foo() {
|
||||
return _ctx.foo
|
||||
},
|
||||
get id() {
|
||||
return _ctx.id
|
||||
},
|
||||
},
|
||||
n0 as any, // TODO: type
|
||||
)
|
||||
return n0
|
||||
},
|
||||
})
|
||||
|
||||
const instace = render(Comp, {}, host)
|
||||
const reset = setCurrentInstance(instace)
|
||||
// expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div>1</div>')
|
||||
|
||||
foo.value++
|
||||
await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
|
||||
expect(host.innerHTML).toBe('<div>2</div>')
|
||||
|
||||
// id.value = 'b'
|
||||
// await nextTick()
|
||||
// expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
|
||||
reset()
|
||||
})
|
||||
|
||||
test.todo('validator', () => {
|
||||
// TODO: impl validator
|
||||
})
|
||||
|
||||
test.todo('warn props mutation', () => {
|
||||
// TODO: impl warn
|
||||
})
|
||||
|
||||
test.todo('warn absent required props', () => {
|
||||
// TODO: impl warn
|
||||
})
|
||||
|
||||
test.todo('warn on type mismatch', () => {
|
||||
// TODO: impl warn
|
||||
})
|
||||
|
||||
// #3495
|
||||
test.todo('should not warn required props using kebab-case', async () => {
|
||||
// TODO: impl warn
|
||||
})
|
||||
|
||||
test('props type support BigInt', () => {
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
foo: BigInt,
|
||||
},
|
||||
render() {
|
||||
const instance = getCurrentInstance()!
|
||||
const t0 = template('<div></div>')
|
||||
const n0 = t0()
|
||||
const {
|
||||
0: [n1],
|
||||
} = children(n0)
|
||||
watchEffect(() => {
|
||||
setText(n1, instance.props.foo)
|
||||
})
|
||||
return n0
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
Comp,
|
||||
{
|
||||
get foo() {
|
||||
return (
|
||||
BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000)
|
||||
)
|
||||
},
|
||||
},
|
||||
'#host',
|
||||
)
|
||||
expect(host.innerHTML).toBe('<div>60000000100000111</div>')
|
||||
})
|
||||
|
||||
// #3288
|
||||
test.todo(
|
||||
'declared prop key should be present even if not passed',
|
||||
async () => {
|
||||
// let initialKeys: string[] = []
|
||||
// const changeSpy = vi.fn()
|
||||
// const passFoo = ref(false)
|
||||
// const Comp = {
|
||||
// props: ['foo'],
|
||||
// setup() {
|
||||
// const instance = getCurrentInstance()!
|
||||
// initialKeys = Object.keys(instance.props)
|
||||
// watchEffect(changeSpy)
|
||||
// return {}
|
||||
// },
|
||||
// render() {
|
||||
// return {}
|
||||
// },
|
||||
// }
|
||||
// const Parent = createIf(
|
||||
// () => passFoo.value,
|
||||
// () => {
|
||||
// return render(Comp , { foo: 1 }, host) // TODO: createComponent fn
|
||||
// },
|
||||
// )
|
||||
// // expect(changeSpy).toHaveBeenCalledTimes(1)
|
||||
},
|
||||
)
|
||||
|
||||
// #3371
|
||||
test.todo(`avoid double-setting props when casting`, async () => {
|
||||
// TODO: proide, slots
|
||||
})
|
||||
|
||||
test('support null in required + multiple-type declarations', () => {
|
||||
const Comp = defineComponent({
|
||||
props: {
|
||||
foo: { type: [Function, null], required: true },
|
||||
},
|
||||
render() {},
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
render(Comp, { foo: () => {} }, host)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(() => {
|
||||
render(Comp, { foo: null }, host)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
// #5016
|
||||
test.todo('handling attr with undefined value', () => {
|
||||
// TODO: attrs
|
||||
})
|
||||
|
||||
// #6915
|
||||
test('should not mutate original props long-form definition object', () => {
|
||||
const props = {
|
||||
msg: {
|
||||
type: String,
|
||||
},
|
||||
}
|
||||
const Comp = defineComponent({
|
||||
props,
|
||||
render() {},
|
||||
})
|
||||
|
||||
render(Comp, { msg: 'test' }, host)
|
||||
|
||||
expect(Object.keys(props.msg).length).toBe(1)
|
||||
})
|
||||
})
|
|
@ -13,7 +13,11 @@ import {
|
|||
isReservedProp,
|
||||
} from '@vue/shared'
|
||||
import { shallowReactive, toRaw } from '@vue/reactivity'
|
||||
import type { Component, ComponentInternalInstance } from './component'
|
||||
import {
|
||||
type Component,
|
||||
type ComponentInternalInstance,
|
||||
setCurrentInstance,
|
||||
} from './component'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
|
@ -165,15 +169,9 @@ function resolvePropValue(
|
|||
// if (key in propsDefaults) {
|
||||
// value = propsDefaults[key]
|
||||
// } else {
|
||||
// setCurrentInstance(instance)
|
||||
// value = propsDefaults[key] = defaultValue.call(
|
||||
// __COMPAT__ &&
|
||||
// isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
|
||||
// ? createPropsDefaultThis(instance, props, key)
|
||||
// : null,
|
||||
// props,
|
||||
// )
|
||||
// unsetCurrentInstance()
|
||||
const reset = setCurrentInstance(instance)
|
||||
value = defaultValue.call(null, props)
|
||||
reset()
|
||||
// }
|
||||
} else {
|
||||
value = defaultValue
|
||||
|
|
Loading…
Reference in New Issue