From 1a8027a517893e7abf0c753b7e8dfe4d67e43f16 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 27 Oct 2025 11:31:24 +0800 Subject: [PATCH] wip: save --- packages/runtime-dom/src/apiCustomElement.ts | 39 +- .../__tests__/customElement.spec.ts | 1960 +++++++++++++++++ .../src/apiDefineVaporCustomElement.ts | 36 +- packages/runtime-vapor/src/component.ts | 10 +- packages/runtime-vapor/src/renderEffect.ts | 2 + 5 files changed, 2002 insertions(+), 45 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/customElement.spec.ts diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 61c3d7c441..892ba8d9e0 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -322,6 +322,10 @@ export abstract class VueElementBase< } } + get _isVapor(): boolean { + return `__vapor` in this._def + } + protected _setParent( parent: VueElementBase | undefined = this._parent, ): void { @@ -455,7 +459,22 @@ export abstract class VueElementBase< } } - protected _processEmit(): void { + protected _processInstance(): void { + this._instance!.ce = this + this._instance!.isCE = true + + if (__DEV__) { + this._instance!.ceReload = newStyles => { + if (this._styles) { + this._styles.forEach(s => this._root.removeChild(s)) + this._styles.length = 0 + } + this._applyStyles(newStyles) + this._instance = null + this._update() + } + } + const dispatch = (event: string, args: any[]) => { this.dispatchEvent( new CustomEvent( @@ -530,7 +549,7 @@ export abstract class VueElementBase< if (val === REMOVAL) { delete this._props[key] } else { - this._props[key] = val + this._props[key] = this._isVapor ? () => val : val // support set key on ceVNode if (key === 'key' && this._app && this._app._ceVNode) { this._app._ceVNode!.key = val @@ -746,21 +765,7 @@ export class VueElement extends VueElementBase< if (!this._instance) { vnode.ce = instance => { this._instance = instance - instance.ce = this - instance.isCE = true - if (__DEV__) { - instance.ceReload = newStyles => { - if (this._styles) { - this._styles.forEach(s => this._root.removeChild(s)) - this._styles.length = 0 - } - this._applyStyles(newStyles) - this._instance = null - this._update() - } - } - - this._processEmit() + this._processInstance() this._setParent() } } diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts new file mode 100644 index 0000000000..dbe79a53d6 --- /dev/null +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -0,0 +1,1960 @@ +// import type { MockedFunction } from 'vitest' +import type { VaporElement } from '../src/apiDefineVaporCustomElement' +import { + type HMRRuntime, + nextTick, + ref, + toDisplayString, +} from '@vue/runtime-dom' +import { + createComponentWithFallback, + createVaporApp, + defineVaporComponent, + defineVaporCustomElement, + delegateEvents, + renderEffect, + setInsertionState, + setText, + setValue, + template, + txt, +} from '../src' + +declare var __VUE_HMR_RUNTIME__: HMRRuntime + +describe('defineVaporCustomElement', () => { + const container = document.createElement('div') + document.body.appendChild(container) + + delegateEvents('input') + + beforeEach(() => { + container.innerHTML = '' + }) + + describe('mounting/unmount', () => { + const E = defineVaporCustomElement({ + props: { + msg: { + type: String, + default: 'hello', + }, + }, + setup(props: any) { + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, toDisplayString(props.msg))) + return n0 + }, + }) + customElements.define('my-element', E) + + test('should work', () => { + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + expect(e).toBeInstanceOf(E) + expect(e._instance).toBeTruthy() + expect(e.shadowRoot!.innerHTML).toBe(`
hello
`) + }) + + test('should work w/ manual instantiation', () => { + const e = new E({ msg: () => 'inline' }) + // should lazy init + expect(e._instance).toBe(null) + // should initialize on connect + container.appendChild(e) + expect(e._instance).toBeTruthy() + expect(e.shadowRoot!.innerHTML).toBe(`
inline
`) + }) + + test('should unmount on remove', async () => { + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + container.removeChild(e) + await nextTick() + expect(e._instance).toBe(null) + expect(e.shadowRoot!.innerHTML).toBe('') + }) + + test('When elements move, avoid prematurely disconnecting MutationObserver', async () => { + const CustomInput = defineVaporCustomElement({ + props: ['value'], + emits: ['update'], + setup(props: any, { emit }: any) { + const n0 = template('', true)() as any + n0.$evtinput = () => { + const num = (n0 as HTMLInputElement).valueAsNumber + emit('update', Number.isNaN(num) ? null : num) + } + renderEffect(() => { + setValue(n0, props.value) + }) + return n0 + // return () => + // h('input', { + // type: 'number', + // value: props.value, + // onInput: (e: InputEvent) => { + // const num = (e.target! as HTMLInputElement).valueAsNumber + // emit('update', Number.isNaN(num) ? null : num) + // }, + // }) + }, + }) + customElements.define('my-el-input', CustomInput) + const num = ref('12') + const containerComp = defineVaporComponent({ + setup() { + // return () => { + // return h('div', [ + // h('my-el-input', { + // value: num.value, + // onUpdate: ($event: CustomEvent) => { + // num.value = $event.detail[0] + // }, + // }), + // h('div', { id: 'move' }), + // ]) + // } + const n1 = template('
', true)() as any + setInsertionState(n1, 0, true) + createComponentWithFallback('my-el-input', { + value: () => num.value, + onInput: () => ($event: CustomEvent) => { + num.value = $event.detail[0] + }, + }) + return n1 + }, + }) + const app = createVaporApp(containerComp) + const container = document.createElement('div') + document.body.appendChild(container) + app.mount(container) + const myInputEl = container.querySelector('my-el-input')! + const inputEl = myInputEl.shadowRoot!.querySelector('input')! + await nextTick() + expect(inputEl.value).toBe('12') + const moveEl = container.querySelector('#move')! + moveEl.append(myInputEl) + await nextTick() + myInputEl.removeAttribute('value') + await nextTick() + expect(inputEl.value).toBe('') + }) + + // test('should not unmount on move', async () => { + // container.innerHTML = `
` + // const e = container.childNodes[0].childNodes[0] as VaporElement + // const i = e._instance + // // moving from one parent to another - this will trigger both disconnect + // // and connected callbacks synchronously + // container.appendChild(e) + // await nextTick() + // // should be the same instance + // expect(e._instance).toBe(i) + // expect(e.shadowRoot!.innerHTML).toBe('
hello
') + // }) + + // test('remove then insert again', async () => { + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // container.removeChild(e) + // await nextTick() + // expect(e._instance).toBe(null) + // expect(e.shadowRoot!.innerHTML).toBe('') + // container.appendChild(e) + // expect(e._instance).toBeTruthy() + // expect(e.shadowRoot!.innerHTML).toBe('
hello
') + // }) + }) + + // describe('props', () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: [String, null], + // bar: Object, + // bazQux: null, + // value: null, + // }, + // render() { + // return [ + // h('div', null, this.foo || ''), + // h('div', null, this.bazQux || (this.bar && this.bar.x)), + // ] + // }, + // }) + // customElements.define('my-el-props', E) + + // test('renders custom element w/ correct object prop value', () => { + // render(h('my-el-props', { value: { x: 1 } }), container) + // const el = container.children[0] + // expect((el as any).value).toEqual({ x: 1 }) + // }) + + // test('props via attribute', async () => { + // // bazQux should map to `baz-qux` attribute + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe('
hello
bye
') + + // // change attr + // e.setAttribute('foo', 'changed') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
changed
bye
') + + // e.setAttribute('baz-qux', 'changed') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe( + // '
changed
changed
', + // ) + // }) + + // test('props via properties', async () => { + // const e = new E() + // e.foo = 'one' + // e.bar = { x: 'two' } + // container.appendChild(e) + // expect(e.shadowRoot!.innerHTML).toBe('
one
two
') + + // // reflect + // // should reflect primitive value + // expect(e.getAttribute('foo')).toBe('one') + // // should not reflect rich data + // expect(e.hasAttribute('bar')).toBe(false) + + // e.foo = 'three' + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
three
two
') + // expect(e.getAttribute('foo')).toBe('three') + + // e.foo = null + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
two
') + // expect(e.hasAttribute('foo')).toBe(false) + + // e.foo = undefined + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
two
') + // expect(e.hasAttribute('foo')).toBe(false) + // expect(e.foo).toBe(undefined) + + // e.bazQux = 'four' + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
four
') + // expect(e.getAttribute('baz-qux')).toBe('four') + // }) + + // test('props via attributes and properties changed together', async () => { + // const e = new E() + // e.foo = 'foo1' + // e.bar = { x: 'bar1' } + // container.appendChild(e) + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
foo1
bar1
') + + // // change attr then property + // e.setAttribute('foo', 'foo2') + // e.bar = { x: 'bar2' } + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
foo2
bar2
') + // expect(e.getAttribute('foo')).toBe('foo2') + // expect(e.hasAttribute('bar')).toBe(false) + + // // change prop then attr + // e.bar = { x: 'bar3' } + // e.setAttribute('foo', 'foo3') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe('
foo3
bar3
') + // expect(e.getAttribute('foo')).toBe('foo3') + // expect(e.hasAttribute('bar')).toBe(false) + // }) + + // test('props via hyphen property', async () => { + // const Comp = defineVaporCustomElement({ + // props: { + // fooBar: Boolean, + // }, + // render() { + // return 'Comp' + // }, + // }) + // customElements.define('my-el-comp', Comp) + // render(h('my-el-comp', { 'foo-bar': true }), container) + // const el = container.children[0] + // expect((el as any).outerHTML).toBe('') + // }) + + // test('attribute -> prop type casting', async () => { + // const E = defineVaporCustomElement({ + // props: { + // fooBar: Number, // test casting of camelCase prop names + // bar: Boolean, + // baz: String, + // }, + // render() { + // return [ + // this.fooBar, + // typeof this.fooBar, + // this.bar, + // typeof this.bar, + // this.baz, + // typeof this.baz, + // ].join(' ') + // }, + // }) + // customElements.define('my-el-props-cast', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe( + // `1 number false boolean 12345 string`, + // ) + + // e.setAttribute('bar', '') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`) + + // e.setAttribute('foo-bar', '2e1') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe( + // `20 number true boolean 12345 string`, + // ) + + // e.setAttribute('baz', '2e1') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`) + // }) + + // // #4772 + // test('attr casting w/ programmatic creation', () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: Number, + // }, + // render() { + // return `foo type: ${typeof this.foo}` + // }, + // }) + // customElements.define('my-element-programmatic', E) + // const el = document.createElement('my-element-programmatic') as any + // el.setAttribute('foo', '123') + // container.appendChild(el) + // expect(el.shadowRoot.innerHTML).toBe(`foo type: number`) + // }) + + // test('handling properties set before upgrading', () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: String, + // dataAge: Number, + // }, + // setup(props) { + // expect(props.foo).toBe('hello') + // expect(props.dataAge).toBe(5) + // }, + // render() { + // return h('div', `foo: ${this.foo}`) + // }, + // }) + // const el = document.createElement('my-el-upgrade') as any + // el.foo = 'hello' + // el.dataset.age = 5 + // el.notProp = 1 + // container.appendChild(el) + // customElements.define('my-el-upgrade', E) + // expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`) + // // should not reflect if not declared as a prop + // expect(el.hasAttribute('not-prop')).toBe(false) + // }) + + // test('handle properties set before connecting', () => { + // const obj = { a: 1 } + // const E = defineVaporCustomElement({ + // props: { + // foo: String, + // post: Object, + // }, + // setup(props) { + // expect(props.foo).toBe('hello') + // expect(props.post).toBe(obj) + // }, + // render() { + // return JSON.stringify(this.post) + // }, + // }) + // customElements.define('my-el-preconnect', E) + // const el = document.createElement('my-el-preconnect') as any + // el.foo = 'hello' + // el.post = obj + + // container.appendChild(el) + // expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj)) + // }) + + // // https://github.com/vuejs/core/issues/6163 + // test('handle components with no props', async () => { + // const E = defineVaporCustomElement({ + // render() { + // return h('div', 'foo') + // }, + // }) + // customElements.define('my-element-noprops', E) + // const el = document.createElement('my-element-noprops') + // container.appendChild(el) + // await nextTick() + // expect(el.shadowRoot!.innerHTML).toMatchInlineSnapshot('"
foo
"') + // }) + + // // #5793 + // test('set number value in dom property', () => { + // const E = defineVaporCustomElement({ + // props: { + // 'max-age': Number, + // }, + // render() { + // // @ts-expect-error + // return `max age: ${this.maxAge}/type: ${typeof this.maxAge}` + // }, + // }) + // customElements.define('my-element-number-property', E) + // const el = document.createElement('my-element-number-property') as any + // container.appendChild(el) + // el.maxAge = 50 + // expect(el.maxAge).toBe(50) + // expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') + // }) + + // // #9006 + // test('should reflect default value', () => { + // const E = defineVaporCustomElement({ + // props: { + // value: { + // type: String, + // default: 'hi', + // }, + // }, + // render() { + // return this.value + // }, + // }) + // customElements.define('my-el-default-val', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as any + // 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('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('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
', + // ) + // }) + // }) + + // describe('attrs', () => { + // const E = defineVaporCustomElement({ + // render() { + // return [h('div', null, this.$attrs.foo as string)] + // }, + // }) + // 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
') + + // 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('
') + // }) + + // // 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) + + // const e = new E() + // container.appendChild(e) + // expect(e.shadowRoot!.delegatesFocus).toBe(true) + // }) + // }) + + // describe('emits', () => { + // const CompDef = defineVaporComponent({ + // setup(_, { emit }) { + // emit('created') + // return () => + // h('div', { + // onClick: () => { + // emit('my-click', 1) + // }, + // onMousedown: () => { + // emit('myEvent', 1) // validate hyphenation + // }, + // onWheel: () => { + // emit('my-wheel', { bubbles: true }, 1) + // }, + // }) + // }, + // }) + // const E = defineVaporCustomElement(CompDef) + // customElements.define('my-el-emits', E) + + // test('emit on connect', () => { + // const e = new E() + // const spy = vi.fn() + // e.addEventListener('created', spy) + // container.appendChild(e) + // expect(spy).toHaveBeenCalled() + // }) + + // test('emit on interaction', () => { + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // const spy = vi.fn() + // e.addEventListener('my-click', spy) + // e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click')) + // expect(spy).toHaveBeenCalledTimes(1) + // expect(spy.mock.calls[0][0]).toMatchObject({ + // detail: [1], + // }) + // }) + + // // #5373 + // test('case transform for camelCase event', () => { + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // const spy1 = vi.fn() + // e.addEventListener('myEvent', spy1) + // const spy2 = vi.fn() + // // emitting myEvent, but listening for my-event. This happens when + // // using the custom element in a Vue template + // e.addEventListener('my-event', spy2) + // e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('mousedown')) + // expect(spy1).toHaveBeenCalledTimes(1) + // expect(spy2).toHaveBeenCalledTimes(1) + // }) + + // test('emit from within async component wrapper', async () => { + // const p = new Promise(res => res(CompDef as any)) + // const E = defineVaporCustomElement(defineAsyncComponent(() => p)) + // customElements.define('my-async-el-emits', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // const spy = vi.fn() + // e.addEventListener('my-click', spy) + // // this feels brittle but seems necessary to reach the node in the DOM. + // await customElements.whenDefined('my-async-el-emits') + // await nextTick() + // await nextTick() + // e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click')) + // expect(spy).toHaveBeenCalled() + // expect(spy.mock.calls[0][0]).toMatchObject({ + // detail: [1], + // }) + // }) + + // // #7293 + // test('emit in an async component wrapper with properties bound', async () => { + // const E = defineVaporCustomElement( + // defineAsyncComponent( + // () => new Promise(res => res(CompDef as any)), + // ), + // ) + // customElements.define('my-async-el-props-emits', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // const spy = vi.fn() + // e.addEventListener('my-click', spy) + // await customElements.whenDefined('my-async-el-props-emits') + // await nextTick() + // await nextTick() + // e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click')) + // expect(spy).toHaveBeenCalled() + // expect(spy.mock.calls[0][0]).toMatchObject({ + // detail: [1], + // }) + // }) + + // test('emit with options', async () => { + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // const spy = vi.fn() + // e.addEventListener('my-wheel', spy) + // e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('wheel')) + // expect(spy).toHaveBeenCalledTimes(1) + // expect(spy.mock.calls[0][0]).toMatchObject({ + // bubbles: true, + // detail: [{ bubbles: true }, 1], + // }) + // }) + // }) + + // describe('slots', () => { + // const E = defineVaporCustomElement({ + // render() { + // return [ + // h('div', null, [ + // renderSlot(this.$slots, 'default', undefined, () => [ + // h('div', 'fallback'), + // ]), + // ]), + // h('div', null, renderSlot(this.$slots, 'named')), + // ] + // }, + // }) + // customElements.define('my-el-slots', E) + + // test('render slots correctly', () => { + // container.innerHTML = `hi` + // const e = container.childNodes[0] as VaporElement + // // native slots allocation does not affect innerHTML, so we just + // // verify that we've rendered the correct native slots here... + // expect(e.shadowRoot!.innerHTML).toBe( + // `
fallback
`, + // ) + // }) + + // test('render slot props', async () => { + // const foo = ref('foo') + // const E = defineVaporCustomElement({ + // render() { + // return [ + // h( + // 'div', + // null, + // renderSlot(this.$slots, 'default', { class: foo.value }), + // ), + // ] + // }, + // }) + // customElements.define('my-el-slot-props', E) + // container.innerHTML = `hi` + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe( + // `
`, + // ) + + // foo.value = 'bar' + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe( + // `
`, + // ) + // }) + // }) + + // describe('provide/inject', () => { + // const Consumer = defineVaporCustomElement({ + // setup() { + // const foo = inject('foo')! + // return () => h('div', foo.value) + // }, + // }) + // customElements.define('my-consumer', Consumer) + + // test('over nested usage', async () => { + // const foo = ref('injected!') + // const Provider = defineVaporCustomElement({ + // provide: { + // foo, + // }, + // render() { + // return h('my-consumer') + // }, + // }) + // customElements.define('my-provider', Provider) + // container.innerHTML = `` + // const provider = container.childNodes[0] as VaporElement + // const consumer = provider.shadowRoot!.childNodes[0] as VaporElement + + // expect(consumer.shadowRoot!.innerHTML).toBe(`
injected!
`) + + // foo.value = 'changed!' + // await nextTick() + // expect(consumer.shadowRoot!.innerHTML).toBe(`
changed!
`) + // }) + + // test('over slot composition', async () => { + // const foo = ref('injected!') + // const Provider = defineVaporCustomElement({ + // provide: { + // foo, + // }, + // render() { + // return renderSlot(this.$slots, 'default') + // }, + // }) + // customElements.define('my-provider-2', Provider) + + // container.innerHTML = `` + // const provider = container.childNodes[0] + // const consumer = provider.childNodes[0] as VaporElement + // expect(consumer.shadowRoot!.innerHTML).toBe(`
injected!
`) + + // foo.value = 'changed!' + // await nextTick() + // expect(consumer.shadowRoot!.innerHTML).toBe(`
changed!
`) + // }) + + // test('inherited from ancestors', async () => { + // const fooA = ref('FooA!') + // const fooB = ref('FooB!') + // const ProviderA = defineVaporCustomElement({ + // provide: { + // fooA, + // }, + // render() { + // return h('provider-b') + // }, + // }) + // const ProviderB = defineVaporCustomElement({ + // provide: { + // fooB, + // }, + // render() { + // return h('my-multi-consumer') + // }, + // }) + + // const Consumer = defineVaporCustomElement({ + // setup() { + // const fooA = inject('fooA')! + // const fooB = inject('fooB')! + // return () => h('div', `${fooA.value} ${fooB.value}`) + // }, + // }) + + // customElements.define('provider-a', ProviderA) + // customElements.define('provider-b', ProviderB) + // customElements.define('my-multi-consumer', Consumer) + // container.innerHTML = `` + // const providerA = container.childNodes[0] as VaporElement + // const providerB = providerA.shadowRoot!.childNodes[0] as VaporElement + // const consumer = providerB.shadowRoot!.childNodes[0] as VaporElement + + // expect(consumer.shadowRoot!.innerHTML).toBe(`
FooA! FooB!
`) + + // fooA.value = 'changedA!' + // fooB.value = 'changedB!' + // await nextTick() + // expect(consumer.shadowRoot!.innerHTML).toBe( + // `
changedA! changedB!
`, + // ) + // }) + + // // #13212 + // test('inherited from app context within nested elements', async () => { + // const outerValues: (string | undefined)[] = [] + // const innerValues: (string | undefined)[] = [] + // const innerChildValues: (string | undefined)[] = [] + + // const Outer = defineVaporCustomElement( + // { + // setup() { + // outerValues.push( + // inject('shared'), + // inject('outer'), + // inject('inner'), + // ) + // }, + // render() { + // return h('div', [renderSlot(this.$slots, 'default')]) + // }, + // }, + // { + // configureApp(app) { + // app.provide('shared', 'shared') + // app.provide('outer', 'outer') + // }, + // }, + // ) + + // const Inner = defineVaporCustomElement( + // { + // setup() { + // // ensure values are not self-injected + // provide('inner', 'inner-child') + + // innerValues.push( + // inject('shared'), + // inject('outer'), + // inject('inner'), + // ) + // }, + // render() { + // return h('div', [renderSlot(this.$slots, 'default')]) + // }, + // }, + // { + // configureApp(app) { + // app.provide('outer', 'override-outer') + // app.provide('inner', 'inner') + // }, + // }, + // ) + + // const InnerChild = defineVaporCustomElement({ + // setup() { + // innerChildValues.push( + // inject('shared'), + // inject('outer'), + // inject('inner'), + // ) + // }, + // render() { + // return h('div') + // }, + // }) + + // customElements.define('provide-from-app-outer', Outer) + // customElements.define('provide-from-app-inner', Inner) + // customElements.define('provide-from-app-inner-child', InnerChild) + + // container.innerHTML = + // '' + + // '' + + // '' + + // '' + + // '' + + // const outer = container.childNodes[0] as VaporElement + // expect(outer.shadowRoot!.innerHTML).toBe('
') + + // expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes( + // 1, + // ) + // expect( + // '[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' + + // 'It will be overwritten with the new value.', + // ).toHaveBeenWarnedTimes(1) + + // expect(outerValues).toEqual(['shared', 'outer', undefined]) + // expect(innerValues).toEqual(['shared', 'override-outer', 'inner']) + // expect(innerChildValues).toEqual([ + // 'shared', + // 'override-outer', + // 'inner-child', + // ]) + // }) + // }) + + // describe('styles', () => { + // function assertStyles(el: VaporElement, css: string[]) { + // const styles = el.shadowRoot?.querySelectorAll('style')! + // expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar + // for (let i = 0; i < css.length; i++) { + // expect(styles[i].textContent).toBe(css[i]) + // } + // } + + // test('should attach styles to shadow dom', async () => { + // const def = defineVaporComponent({ + // __hmrId: 'foo', + // styles: [`div { color: red; }`], + // render() { + // return h('div', 'hello') + // }, + // }) + // const Foo = defineVaporCustomElement(def) + // customElements.define('my-el-with-styles', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // const style = el.shadowRoot?.querySelector('style')! + // expect(style.textContent).toBe(`div { color: red; }`) + + // // hmr + // __VUE_HMR_RUNTIME__.reload('foo', { + // ...def, + // styles: [`div { color: blue; }`, `div { color: yellow; }`], + // } as any) + + // await nextTick() + // assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`]) + // }) + + // test("child components should inject styles to root element's shadow root", async () => { + // const Baz = () => h(Bar) + // const Bar = defineVaporComponent({ + // __hmrId: 'bar', + // styles: [`div { color: green; }`, `div { color: blue; }`], + // render() { + // return 'bar' + // }, + // }) + // const Foo = defineVaporCustomElement({ + // styles: [`div { color: red; }`], + // render() { + // return [h(Baz), h(Baz)] + // }, + // }) + // customElements.define('my-el-with-child-styles', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + + // // inject order should be child -> parent + // assertStyles(el, [ + // `div { color: green; }`, + // `div { color: blue; }`, + // `div { color: red; }`, + // ]) + + // // hmr + // __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, { + // ...Bar, + // styles: [`div { color: red; }`, `div { color: yellow; }`], + // } as any) + + // await nextTick() + // assertStyles(el, [ + // `div { color: red; }`, + // `div { color: yellow; }`, + // `div { color: red; }`, + // ]) + + // __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, { + // ...Bar, + // styles: [`div { color: blue; }`], + // } as any) + // await nextTick() + // assertStyles(el, [`div { color: blue; }`, `div { color: red; }`]) + // }) + + // test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => { + // const Bar = defineVaporComponent({ + // styles: [`div { color: green; }`], + // render() { + // return 'bar' + // }, + // }) + // const Baz = () => h(Bar) + // const Foo = defineVaporCustomElement( + // { + // render() { + // return [h(Baz)] + // }, + // }, + // { shadowRoot: false }, + // ) + + // customElements.define('my-foo-with-shadowroot-false', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // const style = el.shadowRoot?.querySelector('style') + // expect(style).toBeUndefined() + // }) + + // test('with nonce', () => { + // const Foo = defineVaporCustomElement( + // { + // styles: [`div { color: red; }`], + // render() { + // return h('div', 'hello') + // }, + // }, + // { nonce: 'xxx' }, + // ) + // customElements.define('my-el-with-nonce', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // const style = el.shadowRoot?.querySelector('style')! + // expect(style.getAttribute('nonce')).toBe('xxx') + // }) + // }) + + // describe('async', () => { + // test('should work', async () => { + // const loaderSpy = vi.fn() + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // loaderSpy() + // return Promise.resolve({ + // props: ['msg'], + // styles: [`div { color: red }`], + // render(this: any) { + // return h('div', null, this.msg) + // }, + // }) + // }), + // ) + // customElements.define('my-el-async', E) + // container.innerHTML = + // `` + + // `` + + // await new Promise(r => setTimeout(r)) + + // // loader should be called only once + // expect(loaderSpy).toHaveBeenCalledTimes(1) + + // const e1 = container.childNodes[0] as VaporElement + // const e2 = container.childNodes[1] as VaporElement + + // // should inject styles + // expect(e1.shadowRoot!.innerHTML).toBe( + // `
hello
`, + // ) + // expect(e2.shadowRoot!.innerHTML).toBe( + // `
world
`, + // ) + + // // attr + // e1.setAttribute('msg', 'attr') + // await nextTick() + // expect((e1 as any).msg).toBe('attr') + // expect(e1.shadowRoot!.innerHTML).toBe( + // `
attr
`, + // ) + + // // props + // expect(`msg` in e1).toBe(true) + // ;(e1 as any).msg = 'prop' + // expect(e1.getAttribute('msg')).toBe('prop') + // expect(e1.shadowRoot!.innerHTML).toBe( + // `
prop
`, + // ) + // }) + + // test('set DOM property before resolve', async () => { + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // return Promise.resolve({ + // props: ['msg'], + // setup(props) { + // expect(typeof props.msg).toBe('string') + // }, + // render(this: any) { + // return h('div', this.msg) + // }, + // }) + // }), + // ) + // customElements.define('my-el-async-2', E) + + // const e1 = new E() + + // // set property before connect + // e1.msg = 'hello' + + // const e2 = new E() + + // container.appendChild(e1) + // container.appendChild(e2) + + // // set property after connect but before resolve + // e2.msg = 'world' + + // await new Promise(r => setTimeout(r)) + + // expect(e1.shadowRoot!.innerHTML).toBe(`
hello
`) + // expect(e2.shadowRoot!.innerHTML).toBe(`
world
`) + + // e1.msg = 'world' + // expect(e1.shadowRoot!.innerHTML).toBe(`
world
`) + + // e2.msg = 'hello' + // expect(e2.shadowRoot!.innerHTML).toBe(`
hello
`) + // }) + + // test('Number prop casting before resolve', async () => { + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // return Promise.resolve({ + // props: { n: Number }, + // setup(props) { + // expect(props.n).toBe(20) + // }, + // render(this: any) { + // return h('div', this.n + ',' + typeof this.n) + // }, + // }) + // }), + // ) + // customElements.define('my-el-async-3', E) + // container.innerHTML = `` + + // await new Promise(r => setTimeout(r)) + + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe(`
20,number
`) + // }) + + // test('with slots', async () => { + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // return Promise.resolve({ + // render(this: any) { + // return [ + // h('div', null, [ + // renderSlot(this.$slots, 'default', undefined, () => [ + // h('div', 'fallback'), + // ]), + // ]), + // h('div', null, renderSlot(this.$slots, 'named')), + // ] + // }, + // }) + // }), + // ) + // customElements.define('my-el-async-slots', E) + // container.innerHTML = `hi` + + // await new Promise(r => setTimeout(r)) + + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe( + // `
fallback
`, + // ) + // }) + // }) + + // describe('shadowRoot: false', () => { + // const E = defineVaporCustomElement({ + // shadowRoot: false, + // props: { + // msg: { + // type: String, + // default: 'hello', + // }, + // }, + // render() { + // return h('div', this.msg) + // }, + // }) + // customElements.define('my-el-shadowroot-false', E) + + // test('should work', async () => { + // function raf() { + // return new Promise(resolve => { + // requestAnimationFrame(resolve) + // }) + // } + + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // await raf() + // expect(e).toBeInstanceOf(E) + // expect(e._instance).toBeTruthy() + // expect(e.innerHTML).toBe(`
hello
`) + // expect(e.shadowRoot).toBe(null) + // }) + + // const toggle = ref(true) + // const ES = defineVaporCustomElement( + // { + // render() { + // return [ + // renderSlot(this.$slots, 'default'), + // toggle.value ? renderSlot(this.$slots, 'named') : null, + // renderSlot(this.$slots, 'omitted', {}, () => [ + // h('div', 'fallback'), + // ]), + // ] + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-el-shadowroot-false-slots', ES) + + // test('should render slots', async () => { + // container.innerHTML = + // `` + + // `defaulttext` + + // `
named
` + + // `
` + // const e = container.childNodes[0] as VaporElement + // // native slots allocation does not affect innerHTML, so we just + // // verify that we've rendered the correct native slots here... + // expect(e.innerHTML).toBe( + // `defaulttext` + + // `
named
` + + // `
fallback
`, + // ) + + // toggle.value = false + // await nextTick() + // expect(e.innerHTML).toBe( + // `defaulttext` + `` + `
fallback
`, + // ) + // }) + + // test('render nested customElement w/ shadowRoot false', async () => { + // const calls: string[] = [] + + // const Child = defineVaporCustomElement( + // { + // setup() { + // calls.push('child rendering') + // onMounted(() => { + // calls.push('child mounted') + // }) + // }, + // render() { + // return renderSlot(this.$slots, 'default') + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-child', Child) + + // const Parent = defineVaporCustomElement( + // { + // setup() { + // calls.push('parent rendering') + // onMounted(() => { + // calls.push('parent mounted') + // }) + // }, + // render() { + // return renderSlot(this.$slots, 'default') + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-parent', Parent) + + // const App = { + // render() { + // return h('my-parent', null, { + // default: () => [ + // h('my-child', null, { + // default: () => [h('span', null, 'default')], + // }), + // ], + // }) + // }, + // } + // const app = createVaporApp(App) + // app.mount(container) + // await nextTick() + // const e = container.childNodes[0] as VaporElement + // expect(e.innerHTML).toBe( + // `default`, + // ) + // expect(calls).toEqual([ + // 'parent rendering', + // 'parent mounted', + // 'child rendering', + // 'child mounted', + // ]) + // app.unmount() + // }) + + // test('render nested Teleport w/ shadowRoot false', async () => { + // const target = document.createElement('div') + // const Child = defineVaporCustomElement( + // { + // render() { + // return h( + // Teleport, + // { to: target }, + // { + // default: () => [renderSlot(this.$slots, 'default')], + // }, + // ) + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-el-teleport-child', Child) + // const Parent = defineVaporCustomElement( + // { + // render() { + // return renderSlot(this.$slots, 'default') + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-el-teleport-parent', Parent) + + // const App = { + // render() { + // return h('my-el-teleport-parent', null, { + // default: () => [ + // h('my-el-teleport-child', null, { + // default: () => [h('span', null, 'default')], + // }), + // ], + // }) + // }, + // } + // const app = createVaporApp(App) + // app.mount(container) + // await nextTick() + // expect(target.innerHTML).toBe(`default`) + // app.unmount() + // }) + + // test('render two Teleports w/ shadowRoot false', async () => { + // const target1 = document.createElement('div') + // const target2 = document.createElement('span') + // const Child = defineVaporCustomElement( + // { + // render() { + // return [ + // h(Teleport, { to: target1 }, [renderSlot(this.$slots, 'header')]), + // h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), + // ] + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-el-two-teleport-child', Child) + + // const App = { + // render() { + // return h('my-el-two-teleport-child', null, { + // default: () => [ + // h('div', { slot: 'header' }, 'header'), + // h('span', { slot: 'body' }, 'body'), + // ], + // }) + // }, + // } + // const app = createVaporApp(App) + // app.mount(container) + // await nextTick() + // expect(target1.outerHTML).toBe( + // `
header
`, + // ) + // expect(target2.outerHTML).toBe( + // `body`, + // ) + // app.unmount() + // }) + + // test('render two Teleports w/ shadowRoot false (with disabled)', async () => { + // const target1 = document.createElement('div') + // const target2 = document.createElement('span') + // const Child = defineVaporCustomElement( + // { + // render() { + // return [ + // // with disabled: true + // h(Teleport, { to: target1, disabled: true }, [ + // renderSlot(this.$slots, 'header'), + // ]), + // h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), + // ] + // }, + // }, + // { shadowRoot: false }, + // ) + // customElements.define('my-el-two-teleport-child-0', Child) + + // const App = { + // render() { + // return h('my-el-two-teleport-child-0', null, { + // default: () => [ + // h('div', { slot: 'header' }, 'header'), + // h('span', { slot: 'body' }, 'body'), + // ], + // }) + // }, + // } + // const app = createVaporApp(App) + // app.mount(container) + // await nextTick() + // expect(target1.outerHTML).toBe(`
`) + // expect(target2.outerHTML).toBe( + // `body`, + // ) + // app.unmount() + // }) + + // test('toggle nested custom element with shadowRoot: false', async () => { + // customElements.define( + // 'my-el-child-shadow-false', + // defineVaporCustomElement( + // { + // render(ctx: any) { + // return h('div', null, [renderSlot(ctx.$slots, 'default')]) + // }, + // }, + // { shadowRoot: false }, + // ), + // ) + // const ChildWrapper = { + // render() { + // return h('my-el-child-shadow-false', null, 'child') + // }, + // } + + // customElements.define( + // 'my-el-parent-shadow-false', + // defineVaporCustomElement( + // { + // props: { + // isShown: { type: Boolean, required: true }, + // }, + // render(ctx: any, _: any, $props: any) { + // return $props.isShown + // ? h('div', { key: 0 }, [renderSlot(ctx.$slots, 'default')]) + // : null + // }, + // }, + // { shadowRoot: false }, + // ), + // ) + // const ParentWrapper = { + // props: { + // isShown: { type: Boolean, required: true }, + // }, + // render(ctx: any, _: any, $props: any) { + // return h('my-el-parent-shadow-false', { isShown: $props.isShown }, [ + // renderSlot(ctx.$slots, 'default'), + // ]) + // }, + // } + + // const isShown = ref(true) + // const App = { + // render() { + // return h(ParentWrapper, { isShown: isShown.value } as any, { + // default: () => [h(ChildWrapper)], + // }) + // }, + // } + // const container = document.createElement('div') + // document.body.appendChild(container) + // const app = createVaporApp(App) + // app.mount(container) + // expect(container.innerHTML).toBe( + // `` + + // `
` + + // `` + + // `
child
` + + // `
` + + // `
` + + // `
`, + // ) + + // isShown.value = false + // await nextTick() + // expect(container.innerHTML).toBe( + // ``, + // ) + + // isShown.value = true + // await nextTick() + // expect(container.innerHTML).toBe( + // `` + + // `
` + + // `` + + // `
child
` + + // `
` + + // `
` + + // `
`, + // ) + // }) + // }) + + // describe('helpers', () => { + // test('useHost', () => { + // const Foo = defineVaporCustomElement({ + // setup() { + // const host = useHost()! + // host.setAttribute('id', 'host') + // return () => h('div', 'hello') + // }, + // }) + // customElements.define('my-el-use-host', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // expect(el.id).toBe('host') + // }) + + // test('useShadowRoot for style injection', () => { + // const Foo = defineVaporCustomElement({ + // setup() { + // const root = useShadowRoot()! + // const style = document.createElement('style') + // style.innerHTML = `div { color: red; }` + // root.appendChild(style) + // return () => h('div', 'hello') + // }, + // }) + // customElements.define('my-el-use-shadow-root', Foo) + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // const style = el.shadowRoot?.querySelector('style')! + // expect(style.textContent).toBe(`div { color: red; }`) + // }) + // }) + + // describe('expose', () => { + // test('expose w/ options api', async () => { + // const E = defineVaporCustomElement({ + // data() { + // return { + // value: 0, + // } + // }, + // methods: { + // foo() { + // ;(this as any).value++ + // }, + // }, + // expose: ['foo'], + // render(_ctx: any) { + // return h('div', null, _ctx.value) + // }, + // }) + // customElements.define('my-el-expose-options-api', E) + + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement & { + // foo: () => void + // } + // expect(e.shadowRoot!.innerHTML).toBe(`
0
`) + // e.foo() + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`
1
`) + // }) + // test('expose attributes and callback', async () => { + // type SetValue = (value: string) => void + // let fn: MockedFunction + + // const E = defineVaporCustomElement({ + // setup(_, { expose }) { + // const value = ref('hello') + + // const setValue = (fn = vi.fn((_value: string) => { + // value.value = _value + // })) + + // expose({ + // setValue, + // value, + // }) + + // return () => h('div', null, [value.value]) + // }, + // }) + // customElements.define('my-el-expose', E) + + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement & { + // value: string + // setValue: MockedFunction + // } + // expect(e.shadowRoot!.innerHTML).toBe(`
hello
`) + // expect(e.value).toBe('hello') + // expect(e.setValue).toBe(fn!) + // e.setValue('world') + // expect(e.value).toBe('world') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`
world
`) + // }) + + // test('warning when exposing an existing property', () => { + // const E = defineVaporCustomElement({ + // props: { + // value: String, + // }, + // setup(props, { expose }) { + // expose({ + // value: 'hello', + // }) + + // return () => h('div', null, [props.value]) + // }, + // }) + // customElements.define('my-el-expose-two', E) + + // container.innerHTML = `` + + // expect( + // `[Vue warn]: Exposed property "value" already exists on custom element.`, + // ).toHaveBeenWarned() + // }) + // }) + + // test('async & nested custom elements', async () => { + // let fooVal: string | undefined = '' + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // return Promise.resolve({ + // setup(props) { + // provide('foo', 'foo') + // }, + // render(this: any) { + // return h('div', null, [renderSlot(this.$slots, 'default')]) + // }, + // }) + // }), + // ) + + // const EChild = defineVaporCustomElement({ + // setup(props) { + // fooVal = inject('foo') + // }, + // render(this: any) { + // return h('div', null, 'child') + // }, + // }) + // customElements.define('my-el-async-nested-ce', E) + // customElements.define('slotted-child', EChild) + // container.innerHTML = `
` + + // await new Promise(r => setTimeout(r)) + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe(`
`) + // expect(fooVal).toBe('foo') + // }) + + // test('async & multiple levels of nested custom elements', async () => { + // let fooVal: string | undefined = '' + // let barVal: string | undefined = '' + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => { + // return Promise.resolve({ + // setup(props) { + // provide('foo', 'foo') + // }, + // render(this: any) { + // return h('div', null, [renderSlot(this.$slots, 'default')]) + // }, + // }) + // }), + // ) + + // const EChild = defineVaporCustomElement({ + // setup(props) { + // provide('bar', 'bar') + // }, + // render(this: any) { + // return h('div', null, [renderSlot(this.$slots, 'default')]) + // }, + // }) + + // const EChild2 = defineVaporCustomElement({ + // setup(props) { + // fooVal = inject('foo') + // barVal = inject('bar') + // }, + // render(this: any) { + // return h('div', null, 'child') + // }, + // }) + // customElements.define('my-el-async-nested-m-ce', E) + // customElements.define('slotted-child-m', EChild) + // customElements.define('slotted-child2-m', EChild2) + // container.innerHTML = + // `` + + // `
` + + // `` + + // `
` + + // `
` + + // await new Promise(r => setTimeout(r)) + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe(`
`) + // expect(fooVal).toBe('foo') + // expect(barVal).toBe('bar') + // }) + + // describe('configureApp', () => { + // test('should work', () => { + // const E = defineVaporCustomElement( + // () => { + // const msg = inject('msg') + // return () => h('div', msg!) + // }, + // { + // configureApp(app) { + // app.provide('msg', 'app-injected') + // }, + // }, + // ) + // customElements.define('my-element-with-app', E) + + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + + // expect(e.shadowRoot?.innerHTML).toBe('
app-injected
') + // }) + + // // #12448 + // test('work with async component', async () => { + // const AsyncComp = defineAsyncComponent(() => { + // return Promise.resolve({ + // render() { + // const msg: string | undefined = inject('msg') + // return h('div', {}, msg) + // }, + // } as any) + // }) + // const E = defineVaporCustomElement(AsyncComp, { + // configureApp(app) { + // app.provide('msg', 'app-injected') + // }, + // }) + // customElements.define('my-async-element-with-app', E) + + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // await new Promise(r => setTimeout(r)) + // expect(e.shadowRoot?.innerHTML).toBe('
app-injected
') + // }) + + // test('with hmr reload', async () => { + // const __hmrId = '__hmrWithApp' + // const def = defineVaporComponent({ + // __hmrId, + // setup() { + // const msg = inject('msg') + // return { msg } + // }, + // render(this: any) { + // return h('div', [h('span', this.msg), h('span', this.$foo)]) + // }, + // }) + // const E = defineVaporCustomElement(def, { + // configureApp(app) { + // app.provide('msg', 'app-injected') + // app.config.globalProperties.$foo = 'foo' + // }, + // }) + // customElements.define('my-element-with-app-hmr', E) + + // container.innerHTML = `` + // const el = container.childNodes[0] as VaporElement + // expect(el.shadowRoot?.innerHTML).toBe( + // `
app-injectedfoo
`, + // ) + + // // hmr + // __VUE_HMR_RUNTIME__.reload(__hmrId, def as any) + + // await nextTick() + // expect(el.shadowRoot?.innerHTML).toBe( + // `
app-injectedfoo
`, + // ) + // }) + // }) + + // // #9885 + // test('avoid double mount when prop is set immediately after mount', () => { + // customElements.define( + // 'my-input-dupe', + // defineVaporCustomElement({ + // props: { + // value: String, + // }, + // render() { + // return 'hello' + // }, + // }), + // ) + // const container = document.createElement('div') + // document.body.appendChild(container) + // createVaporApp({ + // render() { + // return h('div', [ + // h('my-input-dupe', { + // onVnodeMounted(vnode) { + // vnode.el!.value = 'fesfes' + // }, + // }), + // ]) + // }, + // }).mount(container) + // expect(container.children[0].children[0].shadowRoot?.innerHTML).toBe( + // 'hello', + // ) + // }) + + // // #11081 + // test('Props can be casted when mounting custom elements in component rendering functions', async () => { + // const E = defineVaporCustomElement( + // defineAsyncComponent(() => + // Promise.resolve({ + // props: ['fooValue'], + // setup(props) { + // expect(props.fooValue).toBe('fooValue') + // return () => h('div', props.fooValue) + // }, + // }), + // ), + // ) + // customElements.define('my-el-async-4', E) + // const R = defineVaporComponent({ + // setup() { + // const fooValue = ref('fooValue') + // return () => { + // return h('div', null, [ + // h('my-el-async-4', { + // fooValue: fooValue.value, + // }), + // ]) + // } + // }, + // }) + + // const app = createVaporApp(R) + // app.mount(container) + // await new Promise(r => setTimeout(r)) + // const e = container.querySelector('my-el-async-4') as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe(`
fooValue
`) + // app.unmount() + // }) + + // // #11276 + // test('delete prop on attr removal', async () => { + // const E = defineVaporCustomElement({ + // props: { + // boo: { + // type: Boolean, + // }, + // }, + // render() { + // return this.boo + ',' + typeof this.boo + // }, + // }) + // customElements.define('el-attr-removal', E) + // container.innerHTML = '' + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe(`true,boolean`) + // e.removeAttribute('boo') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`) + // }) + + // test('hyphenated attr removal', async () => { + // const E = defineVaporCustomElement({ + // props: { + // fooBar: { + // type: Boolean, + // }, + // }, + // render() { + // return this.fooBar + // }, + // }) + // customElements.define('el-hyphenated-attr-removal', E) + // const toggle = ref(true) + // const Comp = { + // render() { + // return h('el-hyphenated-attr-removal', { + // 'foo-bar': toggle.value ? '' : null, + // }) + // }, + // } + // render(h(Comp), container) + // const el = container.children[0] + // expect(el.hasAttribute('foo-bar')).toBe(true) + // expect((el as any).outerHTML).toBe( + // ``, + // ) + + // toggle.value = false + // await nextTick() + // expect(el.hasAttribute('foo-bar')).toBe(false) + // expect((el as any).outerHTML).toBe( + // ``, + // ) + // }) + + // test('no unexpected mutation of the 1st argument', () => { + // const Foo = { + // name: 'Foo', + // } + + // defineVaporCustomElement(Foo, { shadowRoot: false }) + + // expect(Foo).toEqual({ + // name: 'Foo', + // }) + // }) +}) diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts index 8cc1782c44..da4a934b06 100644 --- a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts +++ b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts @@ -6,12 +6,10 @@ import { VueElementBase, warn, } from '@vue/runtime-dom' -import { - type ObjectVaporComponent, - type VaporComponent, - type VaporComponentInstance, - mountComponent, - unmountComponent, +import type { + ObjectVaporComponent, + VaporComponent, + VaporComponentInstance, } from './component' export type VaporElementConstructor

= { @@ -83,6 +81,7 @@ export class VaporElement extends VueElementBase< def.name = 'VaporElement' } + def.isCE = true this._app = this._createApp(this._def) this._inheritParentContext() if (this._def.configureApp) { @@ -95,10 +94,10 @@ export class VaporElement extends VueElementBase< protected _update(): void { if (!this._app) return - unmountComponent(this._instance! as VaporComponentInstance, this._root) - const instance = this._createComponent() - instance.appContext = this._app!._context - mountComponent(this._instance! as VaporComponentInstance, this._root) + // update component by re-running all its render effects + const renderEffects = (this._instance! as VaporComponentInstance) + .renderEffects + if (renderEffects) renderEffects.forEach(e => e.run()) } protected _unmount(): void { @@ -112,22 +111,7 @@ export class VaporElement extends VueElementBase< this._instance!.m = this._instance!.u = [this._renderSlots.bind(this)] } - this._instance.ce = this - this._instance.isCE = true - - if (__DEV__) { - this._instance.ceReload = newStyles => { - if (this._styles) { - this._styles.forEach(s => this._root.removeChild(s)) - this._styles.length = 0 - } - this._applyStyles(newStyles) - this._instance = null - this._update() - } - } - - this._processEmit() + this._processInstance() this._setParent() return this._instance diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index e9e288031f..f335256dc4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -127,6 +127,7 @@ export interface ObjectVaporComponent name?: string vapor?: boolean + isCE?: boolean __asyncLoader?: () => Promise __asyncResolved?: VaporComponent } @@ -489,8 +490,15 @@ export class VaporComponentInstance implements GenericComponentInstance { // for suspense suspense: SuspenseBoundary | null + // for HMR and vapor custom element + // all render effects associated with this instance + renderEffects?: RenderEffect[] + hasFallthrough: boolean + // for keep-alive + shapeFlag?: number + // lifecycle hooks isMounted: boolean isUnmounted: boolean @@ -517,12 +525,10 @@ export class VaporComponentInstance implements GenericComponentInstance { devtoolsRawSetupState?: any hmrRerender?: () => void hmrReload?: (newComp: VaporComponent) => void - renderEffects?: RenderEffect[] parentTeleport?: TeleportFragment | null propsOptions?: NormalizedPropsOptions emitsOptions?: ObjectEmitsOptions | null isSingleRoot?: boolean - shapeFlag?: number constructor( comp: VaporComponent, diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 3c937c0ed5..8aece8ee92 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -41,7 +41,9 @@ export class RenderEffect extends ReactiveEffect { this.onTrigger = instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0 + } + if (__DEV__ || instance.type.isCE) { // register effect for stopping them during HMR rerender ;(instance.renderEffects || (instance.renderEffects = [])).push(this) } -- 2.47.3