From 225428c7cb6a9b01e72482974833a86a35e1ca66 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 28 Oct 2025 09:35:09 +0800 Subject: [PATCH] test: add more tests --- .../__tests__/customElement.spec.ts | 256 ++++++++++-------- packages/runtime-vapor/src/componentProps.ts | 14 +- packages/runtime-vapor/src/dom/prop.ts | 12 +- 3 files changed, 151 insertions(+), 131 deletions(-) diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index e83bb70042..843b4ace8d 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -7,11 +7,13 @@ import { toDisplayString, } from '@vue/runtime-dom' import { + child, createComponentWithFallback, createVaporApp, defineVaporComponent, defineVaporCustomElement, delegateEvents, + next, renderEffect, setInsertionState, setText, @@ -467,132 +469,152 @@ describe('defineVaporCustomElement', () => { expect(e.value).toBe('hi') }) - // // #12214 - // test('Boolean prop with default true', async () => { - // const E = defineVaporCustomElement({ - // props: { - // foo: { - // type: Boolean, - // default: true, - // }, - // }, - // render() { - // return String(this.foo) - // }, - // }) - // customElements.define('my-el-default-true', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as HTMLElement & { foo: any }, - // shadowRoot = e.shadowRoot as ShadowRoot - // expect(shadowRoot.innerHTML).toBe('true') - // e.foo = undefined - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('true') - // e.foo = false - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('false') - // e.foo = null - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('null') - // e.foo = '' - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('true') - // }) + test('Boolean prop with default true', async () => { + const E = defineVaporCustomElement({ + props: { + foo: { + type: Boolean, + default: true, + }, + }, + setup(props: any) { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, String(props.foo))) + return n0 + }, + }) + customElements.define('my-el-default-true', E) + container.innerHTML = `` + const e = container.childNodes[0] as HTMLElement & { foo: any }, + shadowRoot = e.shadowRoot as ShadowRoot + expect(shadowRoot.innerHTML).toBe('true') + e.foo = undefined + await nextTick() + expect(shadowRoot.innerHTML).toBe('true') + e.foo = false + await nextTick() + expect(shadowRoot.innerHTML).toBe('false') + e.foo = null + await nextTick() + expect(shadowRoot.innerHTML).toBe('null') + e.foo = '' + await nextTick() + expect(shadowRoot.innerHTML).toBe('true') + }) - // test('support direct setup function syntax with extra options', () => { - // const E = defineVaporCustomElement( - // props => { - // return () => props.text - // }, - // { - // props: { - // text: String, - // }, - // }, - // ) - // customElements.define('my-el-setup-with-props', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // expect(e.shadowRoot!.innerHTML).toBe('hello') - // }) + test('support direct setup function syntax with extra options', () => { + const E = defineVaporCustomElement( + (props: any) => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, props.text)) + return n0 + }, + { + props: { + text: String, + }, + }, + ) + customElements.define('my-el-setup-with-props', E) + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + expect(e.shadowRoot!.innerHTML).toBe('hello') + }) - // test('prop types validation', async () => { - // const E = defineVaporCustomElement({ - // props: { - // num: { - // type: [Number, String], - // }, - // bool: { - // type: Boolean, - // }, - // }, - // render() { - // return h('div', [ - // h('span', [`${this.num} is ${typeof this.num}`]), - // h('span', [`${this.bool} is ${typeof this.bool}`]), - // ]) - // }, - // }) - - // customElements.define('my-el-with-type-props', E) - // render(h('my-el-with-type-props', { num: 1, bool: true }), container) - // const e = container.childNodes[0] as VaporElement - // // @ts-expect-error - // expect(e.num).toBe(1) - // // @ts-expect-error - // expect(e.bool).toBe(true) - // expect(e.shadowRoot!.innerHTML).toBe( - // '
1 is numbertrue is boolean
', - // ) - // }) + test('prop types validation', async () => { + const E = defineVaporCustomElement({ + props: { + num: { + type: [Number, String], + }, + bool: { + type: Boolean, + }, + }, + setup(props: any) { + const n0 = template( + '
', + true, + )() as any + const n1 = child(n0) as any + const n2 = next(n1) as any + const x0 = txt(n1) as any + const x1 = txt(n2) as any + renderEffect(() => setText(x0, `${props.num} is ${typeof props.num}`)) + renderEffect(() => + setText(x1, `${props.bool} is ${typeof props.bool}`), + ) + return n0 + }, + }) + + customElements.define('my-el-with-type-props', E) + const { container } = render('my-el-with-type-props', { + num: () => 1, + bool: () => true, + }) + const e = container.childNodes[0] as VaporElement + // @ts-expect-error + expect(e.num).toBe(1) + // @ts-expect-error + expect(e.bool).toBe(true) + expect(e.shadowRoot!.innerHTML).toBe( + '
1 is numbertrue is boolean
', + ) + }) }) - // describe('attrs', () => { - // const E = defineVaporCustomElement({ - // render() { - // return [h('div', null, this.$attrs.foo as string)] - // }, - // }) - // customElements.define('my-el-attrs', E) + describe('attrs', () => { + const E = defineVaporCustomElement({ + setup(_: any, { attrs }: any) { + const n0 = template('
')() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, toDisplayString(attrs.foo))) + return [n0] + }, + }) + customElements.define('my-el-attrs', E) - // test('attrs via attribute', async () => { - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // expect(e.shadowRoot!.innerHTML).toBe('
hello
') + test('attrs via attribute', async () => { + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + expect(e.shadowRoot!.innerHTML).toBe('
hello
') - // e.setAttribute('foo', 'changed') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
changed
') - // }) + e.setAttribute('foo', 'changed') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
changed
') + }) - // test('non-declared properties should not show up in $attrs', () => { - // const e = new E() - // // @ts-expect-error - // e.foo = '123' - // container.appendChild(e) - // expect(e.shadowRoot!.innerHTML).toBe('
') - // }) + test('non-declared properties should not show up in $attrs', () => { + const e = new E() + // @ts-expect-error + e.foo = '123' + container.appendChild(e) + expect(e.shadowRoot!.innerHTML).toBe('
') + }) - // // https://github.com/vuejs/core/issues/12964 - // // Disabled because of missing support for `delegatesFocus` in jsdom - // // https://github.com/jsdom/jsdom/issues/3418 - // // eslint-disable-next-line vitest/no-disabled-tests - // test.skip('shadowRoot should be initialized with delegatesFocus', () => { - // const E = defineVaporCustomElement( - // { - // render() { - // return [h('input', { tabindex: 1 })] - // }, - // }, - // { shadowRootOptions: { delegatesFocus: true } }, - // ) - // customElements.define('my-el-with-delegate-focus', E) + // https://github.com/vuejs/core/issues/12964 + // Disabled because of missing support for `delegatesFocus` in jsdom + // https://github.com/jsdom/jsdom/issues/3418 + // test.skip('shadowRoot should be initialized with delegatesFocus', () => { + // const E = defineVaporCustomElement( + // { + // // render() { + // // return [h('input', { tabindex: 1 })] + // // }, + // setup() { + // return template('', true)() + // }, + // }, + // { shadowRootOptions: { delegatesFocus: true } }, + // ) + // customElements.define('my-el-with-delegate-focus', E) - // const e = new E() - // container.appendChild(e) - // expect(e.shadowRoot!.delegatesFocus).toBe(true) - // }) - // }) + // const e = new E() + // container.appendChild(e) + // expect(e.shadowRoot!.delegatesFocus).toBe(true) + // }) + }) // describe('emits', () => { // const CompDef = defineVaporComponent({ diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 393e6ef225..496b001d3a 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -217,10 +217,11 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown { } } if (hasOwn(rawProps, key)) { + const value = resolveSource(rawProps[key]) if (merged) { - merged.push(rawProps[key]()) + merged.push(value) } else { - return rawProps[key]() + return value } } if (merged && merged.length) { @@ -318,7 +319,7 @@ export function setupPropsValidation(instance: VaporComponentInstance): void { renderEffect(() => { pushWarningContext(instance) validateProps( - resolveDynamicProps(rawProps, !!instance.type.ce), + resolveDynamicProps(rawProps), instance.props, normalizePropsOptions(instance.type)[0]!, ) @@ -326,14 +327,11 @@ export function setupPropsValidation(instance: VaporComponentInstance): void { }, true /* noLifecycle */) } -export function resolveDynamicProps( - props: RawProps, - isResolved: boolean = false, -): Record { +export function resolveDynamicProps(props: RawProps): Record { const mergedRawProps: Record = {} for (const key in props) { if (key !== '$') { - mergedRawProps[key] = isResolved ? props[key] : props[key]() + mergedRawProps[key] = resolveSource(props[key]) } } if (props.$) { diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index 7642dab962..5aae0560ae 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -485,12 +485,12 @@ export function optimizePropertyLookup(): void { proto.$key = undefined proto.$fc = proto.$evtclick = undefined proto.$root = false - proto.$html = - proto.$txt = - proto.$cls = - proto.$sty = - (Text.prototype as any).$txt = - '' + proto.$html = proto.$cls = proto.$sty = '' + // Initialize $txt to undefined instead of empty string to ensure setText() + // properly updates the text node even when the value is empty string. + // This prevents issues where setText(node, '') would be skipped because + // $txt === '' would return true, leaving the original nodeValue unchanged. + ;(Text.prototype as any).$txt = undefined } function classHasMismatch( -- 2.47.3