From: Evan You Date: Mon, 9 Dec 2024 14:04:15 +0000 (+0800) Subject: test(vapor): componentProps X-Git-Tag: v3.6.0-alpha.1~16^2~185 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=8540ee4af9dbdd9459288706ae467feb74d18864;p=thirdparty%2Fvuejs%2Fcore.git test(vapor): componentProps --- diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index a049889da6..6d0f0ee11a 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -1,11 +1,6 @@ import { createVaporApp, defineVaporComponent } from '../src' import type { App } from '@vue/runtime-dom' -import type { - ObjectVaporComponent, - VaporComponent, - VaporComponentInstance, - VaporSetupFn, -} from '../src/component' +import type { VaporComponent, VaporComponentInstance } from '../src/component' import type { RawProps } from '../src/componentProps' export interface RenderContext { @@ -20,7 +15,7 @@ export interface RenderContext { html: () => string } -export function makeRender( +export function makeRender( initHost = (): HTMLDivElement => { const host = document.createElement('div') host.setAttribute('id', 'host') diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index d468af6671..75d0034716 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -1,5 +1,5 @@ -import { ref, setText, template, watchEffect } from '../src/_old' -import { describe, expect } from 'vitest' +import { ref, watchEffect } from '@vue/runtime-dom' +import { setText, template } from '../src' import { makeRender } from './_utils' const define = makeRender() diff --git a/packages/runtime-vapor/__tests__/componentProps.spec.ts b/packages/runtime-vapor/__tests__/componentProps.spec.ts index dcc67b90e1..520dd9ef72 100644 --- a/packages/runtime-vapor/__tests__/componentProps.spec.ts +++ b/packages/runtime-vapor/__tests__/componentProps.spec.ts @@ -1,33 +1,37 @@ // 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() 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 [] }, }) @@ -51,17 +55,16 @@ describe('component: props', () => { 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 [] }, }) @@ -82,12 +85,13 @@ describe('component: props', () => { 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 }) @@ -108,10 +112,9 @@ describe('component: props', () => { 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 }) @@ -134,9 +137,9 @@ describe('component: props', () => { baz: Boolean, qux: Boolean, }, - render() { - const instance = getCurrentInstance()! - props = instance.props + setup(_props: any) { + props = _props + return [] }, }) @@ -151,7 +154,7 @@ describe('component: props', () => { 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', () => { @@ -172,9 +175,9 @@ describe('component: props', () => { default: defaultBaz, }, }, - render() { - const instance = getCurrentInstance()! - props = instance.props + setup(_props: any) { + props = _props + return [] }, }) @@ -214,18 +217,40 @@ describe('component: props', () => { // 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('
') 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 }, }) @@ -278,7 +303,7 @@ describe('component: props', () => { type: Number, }, }, - render() { + setup() { return t0() }, }).render(props) @@ -286,46 +311,54 @@ describe('component: 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('
') - 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('
') + 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', () => { @@ -336,7 +369,7 @@ describe('component: props', () => { num: { type: Number, required: true }, }, setup() { - return () => null + return [] }, }).render() expect(`Missing required prop: "bool"`).toHaveBeenWarned() @@ -354,7 +387,7 @@ describe('component: props', () => { fooBar: { type: String, required: true }, }, setup() { - return () => null + return [] }, }).render({ ['foo-bar']: () => 'hello', @@ -368,10 +401,9 @@ describe('component: props', () => { 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({ @@ -382,10 +414,44 @@ describe('component: props', () => { }) // #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('

') + 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(`

10

`) + expect(count).toBe(0) + + bar.value++ + await nextTick() + expect(html()).toBe(`

11

`) + expect(count).toBe(0) + }) // #3288 test('declared prop key should be present even if not passed', async () => { @@ -394,7 +460,6 @@ describe('component: props', () => { const passFoo = ref(false) const Comp: any = { - render() {}, props: { foo: String, }, @@ -402,11 +467,14 @@ describe('component: props', () => { 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']) @@ -417,16 +485,17 @@ describe('component: props', () => { // #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(() => { @@ -442,15 +511,11 @@ describe('component: props', () => { test('handling attr with undefined value', () => { const { render, host } = define({ inheritAttrs: false, - render() { - const instance = getCurrentInstance()! + setup(_: any, { attrs }: any) { const t0 = template('
') 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 }, @@ -471,7 +536,7 @@ describe('component: props', () => { type: String, }, } - define({ props, render() {} }).render({ msg: () => 'test' }) + define({ props, setup: () => [] }).render({ msg: () => 'test' }) expect(Object.keys(props.msg).length).toBe(1) }) @@ -481,7 +546,7 @@ describe('component: props', () => { props: { $foo: String, }, - render() {}, + setup: () => [], }) render({ msg: () => 'test' }) diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts index 9d6f1a5b48..fc976f7b5f 100644 --- a/packages/runtime-vapor/src/apiCreateApp.ts +++ b/packages/runtime-vapor/src/apiCreateApp.ts @@ -30,12 +30,12 @@ const unmountApp: AppUnmountFn = app => { unmountComponent(app._instance as VaporComponentInstance, app._container) } -export const createVaporApp: CreateAppFunction< - ParentNode, - VaporComponent -> = comp => { +export const createVaporApp: CreateAppFunction = ( + comp, + props, +) => { if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, i => i) - const app = _createApp(comp) + const app = _createApp(comp, props) const mount = app.mount app.mount = (container, ...args: any[]) => { container = normalizeContainer(container) as ParentNode diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 688fe29f51..501a86b8a6 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -307,8 +307,12 @@ export class VaporComponentInstance implements GenericComponentInstance { this.hasFallthrough = hasFallthroughAttrs(comp, rawProps) if (rawProps || comp.props) { const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp) - this.props = comp.props ? new Proxy(this, propsHandlers!) : {} this.attrs = new Proxy(this, attrsHandlers) + this.props = comp.props + ? new Proxy(this, propsHandlers!) + : isFunction(comp) + ? this.attrs + : EMPTY_OBJ } else { this.props = this.attrs = EMPTY_OBJ } diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 8aa9e0cea4..501770674b 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -1,13 +1,24 @@ -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' @@ -39,15 +50,18 @@ export function getPropsProxyHandlers( } 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.$ @@ -94,9 +108,9 @@ export function getPropsProxyHandlers( 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, @@ -106,11 +120,16 @@ export function getPropsProxyHandlers( } }, ownKeys: () => Object.keys(propsOptions), - set: NO, - deleteProperty: NO, } satisfies ProxyHandler) : 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) @@ -156,10 +175,15 @@ export function getPropsProxyHandlers( } return Array.from(new Set(keys)) }, - set: NO, - deleteProperty: NO, } satisfies ProxyHandler + if (__DEV__) { + Object.assign(attrsHandlers, { + set: propsSetDevTrap, + deleteProperty: propsDeleteDevTrap, + }) + } + return (comp.__propsHandlers = [propsHandlers, attrsHandlers]) } @@ -217,7 +241,11 @@ function resolveDefault( factory: (props: Record) => 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( @@ -278,3 +306,17 @@ export function resolveDynamicProps(props: RawProps): Record { } 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 +}