diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 6760a957f..e5b426c9b 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -711,7 +711,7 @@ describe('component props', () => { ) }) - // #691ef + // #6915 test('should not mutate original props long-form definition object', () => { const props = { msg: { diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts new file mode 100644 index 000000000..a6860db00 --- /dev/null +++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts @@ -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('
') + 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) { + 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('
1
') // TODO: Fallthrough Attributes + expect(host.innerHTML).toBe('
1
') + + foo.value++ + await nextTick() + // expect(host.innerHTML).toBe('
2
') // TODO: Fallthrough Attributes + expect(host.innerHTML).toBe('
2
') + + // id.value = 'b' + // await nextTick() + // expect(host.innerHTML).toBe('
2
') // 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('
') + 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('
60000000100000111
') + }) + + // #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) + }) +}) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 7b77f55bc..efcb81ee8 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -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

= | ComponentObjectPropsOptions

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