// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
import {
- createComponent,
- defineComponent,
- getCurrentInstance,
+ // currentInstance,
+ inject,
nextTick,
+ provide,
ref,
- setText,
- template,
toRefs,
watch,
- watchEffect,
-} from '../src/_old'
+} from '@vue/runtime-dom'
+import {
+ createComponent,
+ defineVaporComponent,
+ renderEffect,
+ setText,
+ template,
+} from '../src'
import { makeRender } from './_utils'
+import type { RawProps } from '../src/componentProps'
const define = makeRender<any>()
describe('component: props', () => {
- // NOTE: no proxy
test('stateful', () => {
let props: any
let attrs: any
const { render } = define({
props: ['fooBar', 'barBaz'],
- render() {
- const instance = getCurrentInstance()!
- props = instance.props
- attrs = instance.attrs
+ setup(_props: any, { attrs: _attrs }: any) {
+ props = _props
+ attrs = _attrs
+ return []
},
})
expect(attrs).toEqual({ qux: 5 })
})
- test.fails('stateful with setup', () => {
+ test('stateful with setup', () => {
let props: any
let attrs: any
const { render } = define({
props: ['foo'],
setup(_props: any, { attrs: _attrs }: any) {
- return () => {
- props = _props
- attrs = _attrs
- }
+ props = _props
+ attrs = _attrs
+ return []
},
})
let props: any
let attrs: any
- const { component: Comp, render } = define((_props: any) => {
- const instance = getCurrentInstance()!
- props = instance.props
- attrs = instance.attrs
- return {}
- })
+ const { component: Comp, render } = define(
+ (_props: any, { attrs: _attrs }: any) => {
+ props = _props
+ attrs = _attrs
+ return []
+ },
+ )
Comp.props = ['foo']
render({ foo: () => 1, bar: () => 2 })
let attrs: any
const { render } = define((_props: any, { attrs: _attrs }: any) => {
- const instance = getCurrentInstance()!
- props = instance.props
- attrs = instance.attrs
- return {}
+ props = _props
+ attrs = _attrs
+ return []
})
render({ foo: () => 1 })
baz: Boolean,
qux: Boolean,
},
- render() {
- const instance = getCurrentInstance()!
- props = instance.props
+ setup(_props: any) {
+ props = _props
+ return []
},
})
expect(props.bar).toBe(true)
expect(props.baz).toBe(true)
expect(props.qux).toBe('ok')
- // expect('type check failed for prop "qux"').toHaveBeenWarned()
+ expect('type check failed for prop "qux"').toHaveBeenWarned()
})
test('default value', () => {
default: defaultBaz,
},
},
- render() {
- const instance = getCurrentInstance()!
- props = instance.props
+ setup(_props: any) {
+ props = _props
+ return []
},
})
// expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 2 times)
})
- test.todo('using inject in default value factory', () => {
- // TODO: impl inject
+ test('using inject in default value factory', () => {
+ let props: any
+
+ const Child = defineVaporComponent({
+ props: {
+ test: {
+ default: () => inject('test', 'default'),
+ },
+ },
+ setup(_props) {
+ props = _props
+ return []
+ },
+ })
+
+ const { render } = define({
+ setup() {
+ provide('test', 'injected')
+ return createComponent(Child)
+ },
+ })
+
+ render()
+
+ expect(props.test).toBe('injected')
})
test('optimized props updates', async () => {
const t0 = template('<div>')
const { component: Child } = define({
props: ['foo'],
- render() {
- const instance = getCurrentInstance()!
+ setup(props: any) {
const n0 = t0()
- watchEffect(() => setText(n0, instance.props.foo))
+ renderEffect(() => setText(n0, props.foo))
return n0
},
})
type: Number,
},
},
- render() {
+ setup() {
return t0()
},
}).render(props)
expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
})
- // TODO: impl setter and warnner
- test.todo(
- 'validator should not be able to mutate other props',
- async () => {
- const mockFn = vi.fn((...args: any[]) => true)
- defineComponent({
- props: {
- foo: {
- type: Number,
- validator: (value: any, props: any) => !!(props.bar = 1),
- },
- bar: {
- type: Number,
- validator: (value: any) => mockFn(value),
- },
- },
- render() {
- const t0 = template('<div/>')
- const n0 = t0()
- return n0
- },
- }).render!({
- foo() {
- return 1
+ test('validator should not be able to mutate other props', async () => {
+ const mockFn = vi.fn((...args: any[]) => true)
+ define({
+ props: {
+ foo: {
+ type: Number,
+ validator: (value: any, props: any) => !!(props.bar = 1),
},
- bar() {
- return 2
+ bar: {
+ type: Number,
+ validator: (value: any) => mockFn(value),
},
- })
+ },
+ setup() {
+ const t0 = template('<div/>')
+ const n0 = t0()
+ return n0
+ },
+ }).render!({
+ foo() {
+ return 1
+ },
+ bar() {
+ return 2
+ },
+ })
- expect(
- `Set operation on key "bar" failed: taris readonly.`,
- ).toHaveBeenWarnedLast()
- expect(mockFn).toHaveBeenCalledWith(2)
- },
- )
+ expect(
+ `Set operation on key "bar" failed: target is readonly.`,
+ ).toHaveBeenWarnedLast()
+ expect(mockFn).toHaveBeenCalledWith(2)
+ })
})
- test.todo('warn props mutation', () => {
- // TODO: impl warn
+ test('warn props mutation', () => {
+ let props: any
+ const { render } = define({
+ props: ['foo'],
+ setup(_props: any) {
+ props = _props
+ return []
+ },
+ })
+ render({ foo: () => 1 })
+ expect(props.foo).toBe(1)
+
+ props.foo = 2
+ expect(`Attempt to mutate prop "foo" failed`).toHaveBeenWarned()
})
test('warn absent required props', () => {
num: { type: Number, required: true },
},
setup() {
- return () => null
+ return []
},
}).render()
expect(`Missing required prop: "bool"`).toHaveBeenWarned()
fooBar: { type: String, required: true },
},
setup() {
- return () => null
+ return []
},
}).render({
['foo-bar']: () => 'hello',
props: {
foo: BigInt,
},
- render() {
- const instance = getCurrentInstance()!
+ setup(props: any) {
const n0 = t0()
- watchEffect(() => setText(n0, instance.props.foo))
+ renderEffect(() => setText(n0, props.foo))
return n0
},
}).render({
})
// #3474
- test.todo(
- 'should cache the value returned from the default factory to avoid unnecessary watcher trigger',
- () => {},
- )
+ test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
+ let count = 0
+
+ const { render, html } = define({
+ props: {
+ foo: {
+ type: Object,
+ default: () => ({ val: 1 }),
+ },
+ bar: Number,
+ },
+ setup(props: any) {
+ watch(
+ () => props.foo,
+ () => {
+ count++
+ },
+ )
+ const t0 = template('<h1></h1>')
+ const n0 = t0()
+ renderEffect(() => {
+ setText(n0, props.foo.val, props.bar)
+ })
+ return n0
+ },
+ })
+
+ const foo = ref()
+ const bar = ref(0)
+ render({ foo: () => foo.value, bar: () => bar.value })
+ expect(html()).toBe(`<h1>10</h1>`)
+ expect(count).toBe(0)
+
+ bar.value++
+ await nextTick()
+ expect(html()).toBe(`<h1>11</h1>`)
+ expect(count).toBe(0)
+ })
// #3288
test('declared prop key should be present even if not passed', async () => {
const passFoo = ref(false)
const Comp: any = {
- render() {},
props: {
foo: String,
},
initialKeys = Object.keys(props)
const { foo } = toRefs(props)
watch(foo, changeSpy)
+ return []
},
}
define(() =>
- createComponent(Comp, [() => (passFoo.value ? { foo: () => 'ok' } : {})]),
+ createComponent(Comp, {
+ $: [() => (passFoo.value ? { foo: 'ok' } : {})],
+ } as RawProps),
).render()
expect(initialKeys).toMatchObject(['foo'])
// #3371
test.todo(`avoid double-setting props when casting`, async () => {
- // TODO: proide, slots
+ // TODO: provide, slots
})
- // NOTE: type check is not supported
- test.todo('support null in required + multiple-type declarations', () => {
+ test('support null in required + multiple-type declarations', () => {
const { render } = define({
props: {
foo: { type: [Function, null], required: true },
},
- render() {},
+ setup() {
+ return []
+ },
})
expect(() => {
test('handling attr with undefined value', () => {
const { render, host } = define({
inheritAttrs: false,
- render() {
- const instance = getCurrentInstance()!
+ setup(_: any, { attrs }: any) {
const t0 = template('<div></div>')
const n0 = t0()
- watchEffect(() =>
- setText(
- n0,
- JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
- ),
+ renderEffect(() =>
+ setText(n0, JSON.stringify(attrs) + Object.keys(attrs)),
)
return n0
},
type: String,
},
}
- define({ props, render() {} }).render({ msg: () => 'test' })
+ define({ props, setup: () => [] }).render({ msg: () => 'test' })
expect(Object.keys(props.msg).length).toBe(1)
})
props: {
$foo: String,
},
- render() {},
+ setup: () => [],
})
render({ msg: () => 'test' })
-import { EMPTY_ARR, NO, YES, camelize, hasOwn, isFunction } from '@vue/shared'
+import {
+ EMPTY_ARR,
+ NO,
+ YES,
+ camelize,
+ hasOwn,
+ isFunction,
+ isString,
+} from '@vue/shared'
import type { VaporComponent, VaporComponentInstance } from './component'
import {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
+ currentInstance,
isEmitListener,
popWarningContext,
pushWarningContext,
resolvePropValue,
+ simpleSetCurrentInstance,
validateProps,
+ warn,
} from '@vue/runtime-dom'
import { normalizeEmitsOptions } from './componentEmits'
import { renderEffect } from './renderEffect'
}
const propsOptions = normalizePropsOptions(comp)[0]
const emitsOptions = normalizeEmitsOptions(comp)
- const isProp = propsOptions
- ? (key: string) => hasOwn(propsOptions, camelize(key))
- : NO
+ const isProp = (
+ propsOptions
+ ? (key: string | symbol) =>
+ isString(key) && hasOwn(propsOptions, camelize(key))
+ : NO
+ ) as (key: string | symbol) => key is string
const isAttr = propsOptions
? (key: string) =>
key !== '$' && !isProp(key) && !isEmitListener(emitsOptions, key)
: YES
- const getProp = (instance: VaporComponentInstance, key: string) => {
+ const getProp = (instance: VaporComponentInstance, key: string | symbol) => {
if (!isProp(key)) return
const rawProps = instance.rawProps
const dynamicSources = rawProps.$
const propsHandlers = propsOptions
? ({
- get: (target, key: string) => getProp(target, key),
- has: (_, key: string) => isProp(key),
- getOwnPropertyDescriptor(target, key: string) {
+ get: (target, key) => getProp(target, key),
+ has: (_, key) => isProp(key),
+ getOwnPropertyDescriptor(target, key) {
if (isProp(key)) {
return {
configurable: true,
}
},
ownKeys: () => Object.keys(propsOptions),
- set: NO,
- deleteProperty: NO,
} satisfies ProxyHandler<VaporComponentInstance>)
: null
+ if (__DEV__ && propsOptions) {
+ Object.assign(propsHandlers!, {
+ set: propsSetDevTrap,
+ deleteProperty: propsDeleteDevTrap,
+ })
+ }
+
const getAttr = (target: RawProps, key: string) => {
if (!isProp(key) && !isEmitListener(emitsOptions, key)) {
return getAttrFromRawProps(target, key)
}
return Array.from(new Set(keys))
},
- set: NO,
- deleteProperty: NO,
} satisfies ProxyHandler<VaporComponentInstance>
+ if (__DEV__) {
+ Object.assign(attrsHandlers, {
+ set: propsSetDevTrap,
+ deleteProperty: propsDeleteDevTrap,
+ })
+ }
+
return (comp.__propsHandlers = [propsHandlers, attrsHandlers])
}
factory: (props: Record<string, any>) => unknown,
instance: VaporComponentInstance,
) {
- return factory.call(null, instance.props)
+ const prev = currentInstance
+ simpleSetCurrentInstance(instance)
+ const res = factory.call(null, instance.props)
+ simpleSetCurrentInstance(prev, instance)
+ return res
}
export function hasFallthroughAttrs(
}
return mergedRawProps
}
+
+function propsSetDevTrap(_: any, key: string | symbol) {
+ warn(
+ `Attempt to mutate prop ${JSON.stringify(key)} failed. Props are readonly.`,
+ )
+ return true
+}
+
+function propsDeleteDevTrap(_: any, key: string | symbol) {
+ warn(
+ `Attempt to delete prop ${JSON.stringify(key)} failed. Props are readonly.`,
+ )
+ return true
+}