import { type HMRRuntime, computed, createApp, h, inject, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, provide, ref, toDisplayString, } from '@vue/runtime-dom' import { compileToVaporRender as compileToFunction, makeRender } from './_utils' import { createComponent, createSlot, createTemplateRefSetter, createVaporApp, defineVaporAsyncComponent, defineVaporComponent, delegateEvents, renderEffect, setText, template, vaporInteropPlugin, withVaporCtx, } from '@vue/runtime-vapor' import { BindingTypes } from '@vue/compiler-core' import type { VaporComponent } from '../src/component' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ const define = makeRender() const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) const triggerEvent = (type: string, el: Element) => { const event = new Event(type, { bubbles: true }) el.dispatchEvent(event) } delegateEvents('click') beforeEach(() => { document.body.innerHTML = '' }) describe('hot module replacement', () => { test('inject global runtime', () => { expect(createRecord).toBeDefined() expect(rerender).toBeDefined() expect(reload).toBeDefined() }) test('createRecord', () => { expect(createRecord('test1', {})).toBe(true) // if id has already been created, should return false expect(createRecord('test1', {})).toBe(false) }) test('rerender', async () => { const root = document.createElement('div') const parentId = 'test2-parent' const childId = 'test2-child' document.body.appendChild(root) const Child = defineVaporComponent({ __hmrId: childId, render: compileToFunction('
'), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: parentId, components: { Child }, setup() { const count = ref(0) return { count } }, render: compileToFunction( `
{{ count }}{{ count }}
`, ), }) createRecord(parentId, Parent as any) const { mount } = define(Parent).create() mount(root) expect(root.innerHTML).toBe(`
0
0
`) // Perform some state change. This change should be preserved after the // re-render! // triggerEvent(root.children[0] as TestElement, 'click') triggerEvent('click', root.children[0]) await nextTick() expect(root.innerHTML).toBe(`
1
1
`) // Update text while preserving state rerender( parentId, compileToFunction( `
{{ count }}!{{ count }}
`, ), ) expect(root.innerHTML).toBe(`
1!
1
`) // Should force child update on slot content change rerender( parentId, compileToFunction( `
{{ count }}!{{ count }}!
`, ), ) expect(root.innerHTML).toBe(`
1!
1!
`) // Should force update element children despite block optimization rerender( parentId, compileToFunction( `
{{ count }}{{ count }} {{ count }}!
`, ), ) expect(root.innerHTML).toBe( `
11
1!
`, ) // Should force update child slot elements rerender( parentId, compileToFunction( `
{{ count }}
`, ), ) expect(root.innerHTML).toBe( `
1
`, ) }) test('reload', async () => { const root = document.createElement('div') const childId = 'test3-child' const unmountSpy = vi.fn() const mountSpy = vi.fn() const Child = defineVaporComponent({ __hmrId: childId, setup() { onUnmounted(unmountSpy) const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: 'parentId', render: () => createComponent(Child), }) define(Parent).create().mount(root) expect(root.innerHTML).toBe(`
0
`) reload(childId, { __hmrId: childId, setup() { onMounted(mountSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) }) test('reload root vapor component should preserve appContext provide/inject', async () => { const root = document.createElement('div') const appId = 'test-root-reload-app-context' const Child = defineVaporComponent({ setup() { const msg = inject('msg') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) const App = defineVaporComponent({ __hmrId: appId, render: () => createComponent(Child), }) createRecord(appId, App as any) const app = createVaporApp(App) app.provide('msg', 'app-injected') app.mount(root) expect(root.innerHTML).toBe(`
app-injected
`) reload(appId, { __vapor: true, __hmrId: appId, render: () => createComponent(Child), }) await nextTick() expect(root.innerHTML).toBe(`
app-injected
`) }) test('reload KeepAlive slot', async () => { const root = document.createElement('div') document.body.appendChild(root) const childId = 'test-child-keep-alive' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactivatedSpy = vi.fn() const Child = defineVaporComponent({ __hmrId: childId, setup() { onUnmounted(unmountSpy) const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: 'parentId', components: { Child }, setup() { const toggle = ref(true) return { toggle } }, render: compileToFunction( `
0
`) reload(childId, { __hmrId: childId, __vapor: true, setup() { onMounted(mountSpy) onUnmounted(unmountSpy) onActivated(activeSpy) onDeactivated(deactivatedSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactivatedSpy).toHaveBeenCalledTimes(1) }) test('reload deactivated KeepAlive child', async () => { const root = document.createElement('div') document.body.appendChild(root) const childId = 'test-child-keep-alive-deactivated' const oldUnmountSpy = vi.fn() const oldActiveSpy = vi.fn() const oldDeactivatedSpy = vi.fn() const newUnmountSpy = vi.fn() const newMountSpy = vi.fn() const newActiveSpy = vi.fn() const newDeactivatedSpy = vi.fn() const Child = defineVaporComponent({ __hmrId: childId, setup() { onUnmounted(oldUnmountSpy) onActivated(oldActiveSpy) onDeactivated(oldDeactivatedSpy) const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: 'parentId-keep-alive-deactivated', components: { Child }, setup() { const toggle = ref(true) return { toggle } }, render: compileToFunction( `
0
`) expect(oldActiveSpy).toHaveBeenCalledTimes(1) expect(oldDeactivatedSpy).toHaveBeenCalledTimes(0) expect(oldUnmountSpy).toHaveBeenCalledTimes(0) // deactivate and move child into KeepAlive cache triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(``) expect(oldDeactivatedSpy).toHaveBeenCalledTimes(1) expect(oldUnmountSpy).toHaveBeenCalledTimes(0) // reload while child is cached but inactive reload(childId, { __hmrId: childId, __vapor: true, setup() { onMounted(newMountSpy) onUnmounted(newUnmountSpy) onActivated(newActiveSpy) onDeactivated(newDeactivatedSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(root.innerHTML).toBe(``) // old cached instance should be unmounted during KeepAlive HMR rerender expect(oldUnmountSpy).toHaveBeenCalledTimes(1) // re-activate should render the new component instance triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(newMountSpy).toHaveBeenCalledTimes(1) expect(newActiveSpy).toHaveBeenCalledTimes(1) expect(newDeactivatedSpy).toHaveBeenCalledTimes(0) // subsequent toggles should use KeepAlive cache for the new instance triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(``) expect(newMountSpy).toHaveBeenCalledTimes(1) expect(newActiveSpy).toHaveBeenCalledTimes(1) expect(newDeactivatedSpy).toHaveBeenCalledTimes(1) triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(newMountSpy).toHaveBeenCalledTimes(1) expect(newActiveSpy).toHaveBeenCalledTimes(2) expect(newDeactivatedSpy).toHaveBeenCalledTimes(1) expect(newUnmountSpy).toHaveBeenCalledTimes(0) }) test('reload KeepAlive slot in Transition', async () => { const root = document.createElement('div') document.body.appendChild(root) const childId = 'test-transition-keep-alive-reload' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactivatedSpy = vi.fn() const Child = defineVaporComponent({ __hmrId: childId, setup() { onUnmounted(unmountSpy) const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: 'parentId', components: { Child }, setup() { const toggle = ref(true) function onLeave(_: any, done: Function) { setTimeout(done, 0) } return { toggle, onLeave } }, render: compileToFunction( `
0
`) reload(childId, { __hmrId: childId, __vapor: true, setup() { onMounted(mountSpy) onUnmounted(unmountSpy) onActivated(activeSpy) onDeactivated(deactivatedSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() await new Promise(r => setTimeout(r, 0)) expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(``) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactivatedSpy).toHaveBeenCalledTimes(1) }) test('reload KeepAlive slot in Transition with out-in', async () => { const root = document.createElement('div') document.body.appendChild(root) const childId = 'test-transition-keep-alive-reload-with-out-in' const unmountSpy = vi.fn() const mountSpy = vi.fn() const activeSpy = vi.fn() const deactivatedSpy = vi.fn() const Child = defineVaporComponent({ name: 'original', __hmrId: childId, setup() { onUnmounted(unmountSpy) const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ components: { Child }, setup() { function onLeave(_: any, done: Function) { setTimeout(done, 0) } const toggle = ref(true) return { toggle, onLeave } }, render: compileToFunction( `
0
`) reload(childId, { name: 'updated', __hmrId: childId, __vapor: true, setup() { onMounted(mountSpy) onUnmounted(unmountSpy) onActivated(activeSpy) onDeactivated(deactivatedSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() await new Promise(r => setTimeout(r, 0)) expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() await new Promise(r => setTimeout(r, 0)) expect(root.innerHTML).toBe(``) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(1) expect(deactivatedSpy).toHaveBeenCalledTimes(1) // should not mount when toggling triggerEvent('click', root.children[0] as Element) await nextTick() expect(root.innerHTML).toBe(`
1
`) expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) expect(activeSpy).toHaveBeenCalledTimes(2) expect(deactivatedSpy).toHaveBeenCalledTimes(1) }) // TODO: renderEffect not re-run after child reload // it requires parent rerender to align with vdom test.todo('reload: avoid infinite recursion', async () => { const root = document.createElement('div') document.body.appendChild(root) const childId = 'test-child-6930' const unmountSpy = vi.fn() const mountSpy = vi.fn() const Child = defineVaporComponent({ __hmrId: childId, setup(_, { expose }) { const count = ref(0) expose({ count, }) onUnmounted(unmountSpy) return { count } }, render: compileToFunction(`
{{ count }}
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ setup() { const com1 = ref() const changeRef1 = (value: any) => (com1.value = value) const com2 = ref() const changeRef2 = (value: any) => (com2.value = value) const setRef = createTemplateRefSetter() const n0 = createComponent(Child) setRef(n0, changeRef1) const n1 = createComponent(Child) setRef(n1, changeRef2) const n2 = template(' ')() as any renderEffect(() => { setText(n2, toDisplayString(com1.value.count)) }) return [n0, n1, n2] }, }) define(Parent).create().mount(root) await nextTick() expect(root.innerHTML).toBe(`
0
0
0`) reload(childId, { __hmrId: childId, __vapor: true, setup() { onMounted(mountSpy) const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await nextTick() expect(root.innerHTML).toBe(`
1
1
1`) expect(unmountSpy).toHaveBeenCalledTimes(2) expect(mountSpy).toHaveBeenCalledTimes(2) }) test('static el reference', async () => { const root = document.createElement('div') document.body.appendChild(root) const id = 'test-static-el' const template = `
{{ count }}
` const Comp = defineVaporComponent({ __hmrId: id, setup() { const count = ref(0) return { count } }, render: compileToFunction(template), }) createRecord(id, Comp as any) define(Comp).create().mount(root) expect(root.innerHTML).toBe(`
0
`) // 1. click to trigger update triggerEvent('click', root.children[0].children[1] as Element) await nextTick() expect(root.innerHTML).toBe(`
1
`) // 2. trigger HMR rerender( id, compileToFunction(template.replace(`
1
`, ) }) test('force update child component w/ static props', () => { const root = document.createElement('div') const parentId = 'test-force-props-parent' const childId = 'test-force-props-child' const Child = defineVaporComponent({ __hmrId: childId, props: { msg: String, }, render: compileToFunction(`
{{ msg }}
`, { bindingMetadata: { msg: BindingTypes.PROPS, }, }), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: parentId, components: { Child }, render: compileToFunction(``), }) createRecord(parentId, Parent as any) define(Parent).create().mount(root) expect(root.innerHTML).toBe(`
foo
`) rerender(parentId, compileToFunction(``)) expect(root.innerHTML).toBe(`
bar
`) }) test('remove static class from parent', () => { const root = document.createElement('div') const parentId = 'test-force-class-parent' const childId = 'test-force-class-child' const Child = defineVaporComponent({ __hmrId: childId, render: compileToFunction(`
child
`), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: parentId, components: { Child }, render: compileToFunction(``), }) createRecord(parentId, Parent as any) define(Parent).create().mount(root) expect(root.innerHTML).toBe(`
child
`) rerender(parentId, compileToFunction(``)) expect(root.innerHTML).toBe(`
child
`) }) test('rerender if any parent in the parent chain', () => { const root = document.createElement('div') const parent = 'test-force-props-parent-' const childId = 'test-force-props-child' const numberOfParents = 5 const Child = defineVaporComponent({ __hmrId: childId, render: compileToFunction(`
child
`), }) createRecord(childId, Child as any) const components: VaporComponent[] = [] for (let i = 0; i < numberOfParents; i++) { const parentId = `${parent}${i}` const parentComp: VaporComponent = { __vapor: true, __hmrId: parentId, } components.push(parentComp) if (i === 0) { parentComp.render = compileToFunction(``) parentComp.components = { Child, } } else { parentComp.render = compileToFunction(``) parentComp.components = { Parent: components[i - 1], } } createRecord(parentId, parentComp as any) } const last = components[components.length - 1] define(last).create().mount(root) expect(root.innerHTML).toBe(`
child
`) rerender(last.__hmrId!, compileToFunction(``)) expect(root.innerHTML).toBe(`
child
`) }) test('rerender with Teleport', () => { const root = document.createElement('div') const target = document.createElement('div') document.body.appendChild(root) document.body.appendChild(target) const parentId = 'parent-teleport' const Child = defineVaporComponent({ setup() { return { target } }, render: compileToFunction(`
`), }) const Parent = { __vapor: true, __hmrId: parentId, components: { Child }, render: compileToFunction(` `), } createRecord(parentId, Parent as any) define(Parent).create().mount(root) expect(root.innerHTML).toBe(``) expect(target.innerHTML).toBe(`
1
`) rerender( parentId, compileToFunction(` `), ) expect(root.innerHTML).toBe(``) expect(target.innerHTML).toBe( `
1
2
`, ) }) test('rerender for component that has no active instance yet', () => { const id = 'no-active-instance-rerender' const Foo = { __vapor: true, __hmrId: id, render: () => template('foo')(), } createRecord(id, Foo) rerender(id, () => template('bar')()) const root = document.createElement('div') define(Foo).create().mount(root) expect(root.innerHTML).toBe('bar') }) test('reload for component that has no active instance yet', () => { const id = 'no-active-instance-reload' const Foo = { __vapor: true, __hmrId: id, render: () => template('foo')(), } createRecord(id, Foo) reload(id, { __hmrId: id, render: () => template('bar')(), }) const root = document.createElement('div') define(Foo).render({}, root) expect(root.innerHTML).toBe('bar') }) test('force update slot content change', () => { const root = document.createElement('div') const parentId = 'test-force-computed-parent' const childId = 'test-force-computed-child' const Child = { __vapor: true, __hmrId: childId, setup(_: any, { slots }: any) { const slotContent = computed(() => { return slots.default?.() }) return { slotContent } }, render: compileToFunction(``), } createRecord(childId, Child) const Parent = { __vapor: true, __hmrId: parentId, components: { Child }, render: compileToFunction(`1`), } createRecord(parentId, Parent) // render(h(Parent), root) define(Parent).render({}, root) expect(root.innerHTML).toBe(`1`) rerender(parentId, compileToFunction(`2`)) expect(root.innerHTML).toBe(`2`) }) // #11248 test('reload async component with multiple instances', async () => { const root = document.createElement('div') const childId = 'test-child-id' const Child = { __vapor: true, __hmrId: childId, setup() { const count = ref(0) return { count } }, render: compileToFunction(`
{{ count }}
`), } const Comp = defineVaporAsyncComponent(() => Promise.resolve(Child)) const appId = 'test-app-id' const App = { __hmrId: appId, render() { return [createComponent(Comp), createComponent(Comp)] }, } createRecord(appId, App) define(App).render({}, root) await timeout() expect(root.innerHTML).toBe( `
0
0
`, ) // change count to 1 reload(childId, { __vapor: true, __hmrId: childId, setup() { const count = ref(1) return { count } }, render: compileToFunction(`
{{ count }}
`), }) await timeout() expect(root.innerHTML).toBe( `
1
1
`, ) }) test.todo('reload async child wrapped in Suspense + KeepAlive', async () => { // const id = 'async-child-reload' // const AsyncChild: ComponentOptions = { // __hmrId: id, // async setup() { // await nextTick() // return () => 'foo' // }, // } // createRecord(id, AsyncChild) // const appId = 'test-app-id' // const App: ComponentOptions = { // __hmrId: appId, // components: { AsyncChild }, // render: compileToFunction(` //
// // // // // //
// `), // } // const root = nodeOps.createElement('div') // render(h(App), root) // expect(serializeInner(root)).toBe('
') // await timeout() // expect(serializeInner(root)).toBe('
foo
') // reload(id, { // __hmrId: id, // async setup() { // await nextTick() // return () => 'bar' // }, // }) // await timeout() // expect(serializeInner(root)).toBe('
bar
') }) test.todo('multi reload child wrapped in Suspense + KeepAlive', async () => { // const id = 'test-child-reload-3' // const Child: ComponentOptions = { // __hmrId: id, // setup() { // const count = ref(0) // return { count } // }, // render: compileToFunction(`
{{ count }}
`), // } // createRecord(id, Child) // const appId = 'test-app-id' // const App: ComponentOptions = { // __hmrId: appId, // components: { Child }, // render: compileToFunction(` // // // // // // `), // } // const root = nodeOps.createElement('div') // render(h(App), root) // expect(serializeInner(root)).toBe('
0
') // await timeout() // reload(id, { // __hmrId: id, // setup() { // const count = ref(1) // return { count } // }, // render: compileToFunction(`
{{ count }}
`), // }) // await timeout() // expect(serializeInner(root)).toBe('
1
') // reload(id, { // __hmrId: id, // setup() { // const count = ref(2) // return { count } // }, // render: compileToFunction(`
{{ count }}
`), // }) // await timeout() // expect(serializeInner(root)).toBe('
2
') }) test('rerender for nested component', () => { const id = 'child-nested-rerender' const Foo = { __vapor: true, __hmrId: id, setup(_ctx: any, { slots }: any) { return slots.default() }, } createRecord(id, Foo) const parentId = 'parent-nested-rerender' const Parent = { __vapor: true, __hmrId: parentId, render() { return createComponent( Foo, {}, { default: withVaporCtx(() => { return createSlot('default') }), }, ) }, } const appId = 'app-nested-rerender' const App = { __vapor: true, __hmrId: appId, render: () => createComponent( Parent, {}, { default: withVaporCtx(() => { return createComponent( Foo, {}, { default: () => template('foo')(), }, ) }), }, ), } createRecord(parentId, App) const root = document.createElement('div') define(App).render({}, root) expect(root.innerHTML).toBe('foo') rerender(id, () => template('bar')()) expect(root.innerHTML).toBe('bar') }) test('reload nested components from single update', async () => { const innerId = 'nested-reload-inner' const outerId = 'nested-reload-outer' let Inner = { __vapor: true, __hmrId: innerId, render() { return template('
foo
')() }, } let Outer = { __vapor: true, __hmrId: outerId, render() { return createComponent(Inner as any) }, } createRecord(innerId, Inner) createRecord(outerId, Outer) const App = { __vapor: true, render: () => createComponent(Outer), } const root = document.createElement('div') define(App).render({}, root) expect(root.innerHTML).toBe('
foo
') Inner = { __vapor: true, __hmrId: innerId, render() { return template('
bar
')() }, } Outer = { __vapor: true, __hmrId: outerId, render() { return createComponent(Inner as any) }, } // trigger reload for both Outer and Inner reload(outerId, Outer) reload(innerId, Inner) await nextTick() expect(root.innerHTML).toBe('
bar
') }) test('child reload + parent reload', async () => { const root = document.createElement('div') const childId = 'test1-child-reload' const parentId = 'test1-parent-reload' const { component: Child } = define({ __hmrId: childId, setup() { const msg = ref('child') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, components: { Child }, setup() { const msg = ref('root') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }).create() createRecord(parentId, Parent as any) mount(root) expect(root.innerHTML).toMatchInlineSnapshot( `"
child
root
"`, ) // reload child reload(childId, { __hmrId: childId, __vapor: true, setup() { const msg = ref('child changed') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed
root
"`, ) // reload child again reload(childId, { __hmrId: childId, __vapor: true, setup() { const msg = ref('child changed2') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed2
root
"`, ) // reload parent reload(parentId, { __hmrId: parentId, __vapor: true, // @ts-expect-error components: { Child }, setup() { const msg = ref('root changed') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed2
root changed
"`, ) }) test('child reload in dynamic branch should not break subsequent parent reload', async () => { const root = document.createElement('div') const childId = 'test-dynamic-child-reload' const parentId = 'test-dynamic-parent-reload' const Child = defineVaporComponent({ __hmrId: childId, setup() { const msg = ref('child') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok } }, render: compileToFunction(``), }).create() createRecord(parentId, Parent as any) mount(root) expect(root.innerHTML).toBe(`
child
`) reload(childId, { __vapor: true, __hmrId: childId, setup() { const msg = ref('child changed') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toBe(`
child changed
`) reload(parentId, { __vapor: true, __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok } }, render: compileToFunction(``), }) await nextTick() expect(root.innerHTML).toBe(`
child changed
`) }) test('child reload with multiple instances in dynamic branch should keep parent reload stable', async () => { const root = document.createElement('div') const childId = 'test-dynamic-multi-child-reload' const parentId = 'test-dynamic-multi-parent-reload' const Child = defineVaporComponent({ __hmrId: childId, setup() { const msg = ref('child') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok } }, render: compileToFunction( ``, ), }).create() createRecord(parentId, Parent as any) mount(root) expect(root.textContent).toBe(`childchild`) reload(childId, { __vapor: true, __hmrId: childId, setup() { const msg = ref('child changed') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(root.textContent).toBe(`child changedchild changed`) reload(parentId, { __vapor: true, __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok } }, render: compileToFunction( ``, ), }) await nextTick() expect(root.textContent).toBe(`child changedchild changed`) }) test('child reload in teleport dynamic branch should not break subsequent parent reload', async () => { const root = document.createElement('div') const target = document.createElement('div') document.body.appendChild(root) document.body.appendChild(target) const childId = 'test-teleport-dynamic-child-reload' const parentId = 'test-teleport-dynamic-parent-reload' const Child = defineVaporComponent({ __hmrId: childId, setup() { const msg = ref('child') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok, target } }, render: compileToFunction( ``, ), }).create() createRecord(parentId, Parent as any) mount(root) expect(target.textContent).toBe(`childsibling`) reload(childId, { __vapor: true, __hmrId: childId, setup() { const msg = ref('child changed') return { msg } }, render: compileToFunction(`
{{ msg }}
`), }) expect(target.textContent).toBe(`child changedsibling`) reload(parentId, { __vapor: true, __hmrId: parentId, components: { Child }, setup() { const ok = ref(true) return { ok, target } }, render: compileToFunction( ``, ), }) await nextTick() expect(target.textContent).toBe(`child changedsibling`) }) // Vapor router-view has no render function (setup-only). // When HMR rerender is triggered, the setup function is re-executed. // Ensure provide() warning is suppressed. test('rerender setup-only component', async () => { const childId = 'test-child-reload-01' const Child = defineVaporComponent({ __hmrId: childId, render: compileToFunction(`
foo
`), }) createRecord(childId, Child as any) // without a render function const Parent = defineVaporComponent({ setup() { provide('foo', 'bar') return createComponent(Child) }, }) const { html } = define({ setup() { return createComponent(Parent) }, }).render() expect(html()).toBe('
foo
') // will trigger parent rerender reload(childId, { __hmrId: childId, render: compileToFunction(`
bar
`), }) await nextTick() expect(html()).toBe('
bar
') expect('provide() can only be used inside setup()').not.toHaveBeenWarned() }) describe('switch vapor/vdom modes', () => { test('vapor -> vdom', async () => { const id = 'vapor-to-vdom' const Comp = { __vapor: true, __hmrId: id, render() { return template('
foo
')() }, } createRecord(id, Comp) const App = { render() { return h(Comp as any) }, } const root = document.createElement('div') const app = createApp(App) app.use(vaporInteropPlugin) app.mount(root) expect(root.innerHTML).toBe('
foo
') // switch to vdom reload(id, { __hmrId: id, render() { return h('div', 'bar') }, }) await nextTick() expect(root.innerHTML).toBe('
bar
') }) test('vdom -> vapor', async () => { const id = 'vdom-to-vapor' const Comp = { __hmrId: id, render() { return h('div', 'foo') }, } createRecord(id, Comp) const App = { render() { return h(Comp) }, } const root = document.createElement('div') const app = createApp(App) app.use(vaporInteropPlugin) app.mount(root) expect(root.innerHTML).toBe('
foo
') // switch to vapor reload(id, { __vapor: true, __hmrId: id, render() { return template('
bar
')() }, }) await nextTick() expect(root.innerHTML).toBe('
bar
') }) }) })