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