test(runtime-vapor): component props (#99)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
ubugeeei 2024-02-04 21:46:08 +09:00 committed by GitHub
parent 8dec243dc1
commit ea5f7ec076
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 496 additions and 11 deletions

View File

@ -711,7 +711,7 @@ describe('component props', () => {
)
})
// #691ef
// #6915
test('should not mutate original props long-form definition object', () => {
const props = {
msg: {

View File

@ -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)
})
})

View File

@ -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