import {
KeepAlive,
type ShallowRef,
Suspense,
Teleport,
createApp,
createVNode,
defineComponent,
h,
inject,
nextTick,
onActivated,
onBeforeMount,
onDeactivated,
onMounted,
onUnmounted,
provide,
ref,
renderSlot,
resolveDynamicComponent,
shallowRef,
toDisplayString,
useModel,
useTemplateRef,
vShow,
withDirectives,
} from '@vue/runtime-dom'
import { makeInteropRender } from './_utils'
import {
VaporKeepAlive,
applyTextModel,
applyVShow,
child,
createComponent,
createDynamicComponent,
createIf,
createSlot,
createTemplateRefSetter,
defineVaporAsyncComponent,
defineVaporComponent,
renderEffect,
setText,
template,
vaporInteropPlugin,
} from '../src'
const define = makeInteropRender()
describe('vdomInterop', () => {
describe('props', () => {
test('should work if props are not provided', () => {
const VaporChild = defineVaporComponent({
props: {
msg: String,
},
setup(_, { attrs }) {
return [document.createTextNode(attrs.class || 'foo')]
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('foo')
})
test('should handle class prop when vapor renders vdom component', () => {
const VDomChild = defineComponent({
setup() {
return () => h('div', { class: 'foo' })
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(VDomChild as any, { class: () => 'bar' })
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('
')
})
})
describe('v-model', () => {
test('basic work', async () => {
const VaporChild = defineVaporComponent({
props: {
modelValue: {},
modelModifiers: {},
},
emits: ['update:modelValue'],
setup(__props) {
const modelValue = useModel(__props, 'modelValue')
const n0 = template('
')() as any
const n1 = template('')() as any
const x0 = child(n0) as any
applyTextModel(
n1,
() => modelValue.value,
_value => (modelValue.value = _value),
)
renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
return [n0, n1]
},
})
const { html, host } = define({
setup() {
const msg = ref('foo')
return () =>
h(VaporChild as any, {
modelValue: msg.value,
'onUpdate:modelValue': (value: string) => {
msg.value = value
},
})
},
}).render()
expect(html()).toBe('foo
')
const inputEl = host.querySelector('input')!
inputEl.value = 'bar'
inputEl.dispatchEvent(new Event('input'))
await nextTick()
expect(html()).toBe('bar
')
})
test('slot v-model should persist when switching vapor/vdom child', async () => {
const VaporComp1 = defineVaporComponent({
name: 'VaporComp1',
setup() {
return [document.createTextNode('comp1: '), createSlot('default')]
},
})
const VDomComp2 = defineComponent({
name: 'VDomComp2',
setup(_, { slots }) {
return () =>
h('div', [
'comp2: ',
// vdom
renderSlot(slots, 'default'),
])
},
})
const VaporParent = defineVaporComponent({
name: 'VaporParent',
props: {
show: Boolean,
modelValue: {},
modelModifiers: {},
},
emits: ['update:modelValue'],
setup(__props) {
const modelValue = useModel(__props, 'modelValue')
return createDynamicComponent(
() => (__props.show ? VaporComp1 : VDomComp2),
null,
{
default: () => {
const input = template('')() as any
applyTextModel(
input,
() => modelValue.value,
_value => (modelValue.value = _value),
)
return input
},
},
true,
)
},
})
const show = ref(true)
const msg = ref('')
const { host } = define({
setup() {
return () =>
h(VaporParent as any, {
show: show.value,
modelValue: msg.value,
'onUpdate:modelValue': (value: string) => {
msg.value = value
},
})
},
}).render()
const input1 = host.querySelector('input')!
input1.value = 'hello'
input1.dispatchEvent(new Event('input'))
await nextTick()
expect(msg.value).toBe('hello')
show.value = false
await nextTick()
const input2 = host.querySelector('input')!
expect(input2.value).toBe('hello')
})
})
describe('emit', () => {
test('emit from vapor child to vdom parent', () => {
const VaporChild = defineVaporComponent({
emits: ['click'],
setup(_, { emit }) {
emit('click')
return []
},
})
const fn = vi.fn()
define({
setup() {
return () => h(VaporChild as any, { onClick: fn })
},
}).render()
// fn should be called once
expect(fn).toHaveBeenCalledTimes(1)
})
})
describe('directives', () => {
test('apply v-show to vdom child', async () => {
const VDomChild = {
setup() {
return () => h('div')
},
}
const show = ref(false)
const VaporChild = defineVaporComponent({
setup() {
const n1 = createComponent(VDomChild as any)
applyVShow(n1, () => show.value)
return n1
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('')
show.value = true
await nextTick()
expect(html()).toBe('')
})
test('apply v-show to vapor child', async () => {
const VaporChild = defineVaporComponent({
setup() {
return template('', true)()
},
})
const show = ref(false)
const App = defineComponent({
setup() {
return () =>
h('div', null, [
withDirectives(h(VaporChild as any), [[vShow, show.value]]),
])
},
})
const root = document.createElement('div')
const app = createApp(App)
app.use(vaporInteropPlugin)
app.mount(root)
expect(root.innerHTML).toBe(
'',
)
show.value = true
await nextTick()
expect(root.innerHTML).toBe('')
})
test('apply custom directive to vapor child', async () => {
const vCustom = {
created: vi.fn(),
beforeMount: vi.fn(),
mounted: vi.fn(),
beforeUpdate: vi.fn(),
updated: vi.fn(),
beforeUnmount: vi.fn(),
unmounted: vi.fn(),
}
const VaporChild = defineVaporComponent({
setup() {
return template('', true)()
},
})
const count = ref(0)
const App = defineComponent({
setup() {
return () =>
h('div', null, [
withDirectives(h(VaporChild as any), [[vCustom, count.value]]),
])
},
})
const root = document.createElement('div')
const app = createApp(App)
app.use(vaporInteropPlugin)
app.mount(root)
// root > div (App root) > div (VaporChild root)
const el = root.querySelector('div')!.querySelector('div')!
expect(vCustom.created).toHaveBeenCalledTimes(1)
expect(vCustom.beforeMount).toHaveBeenCalledTimes(1)
expect(vCustom.mounted).toHaveBeenCalledTimes(1)
expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(0)
expect(vCustom.updated).toHaveBeenCalledTimes(0)
expect(vCustom.created).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 0, oldValue: undefined }),
expect.any(Object),
null,
)
expect(vCustom.beforeMount).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 0, oldValue: undefined }),
expect.any(Object),
null,
)
expect(vCustom.mounted).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 0, oldValue: undefined }),
expect.any(Object),
null,
)
count.value++
await nextTick()
expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(1)
expect(vCustom.updated).toHaveBeenCalledTimes(1)
expect(vCustom.beforeUpdate).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 1, oldValue: 0 }),
expect.any(Object),
expect.any(Object),
)
expect(vCustom.updated).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 1, oldValue: 0 }),
expect.any(Object),
expect.any(Object),
)
app.unmount()
expect(vCustom.beforeUnmount).toHaveBeenCalledTimes(1)
expect(vCustom.unmounted).toHaveBeenCalledTimes(1)
expect(vCustom.beforeUnmount).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 1, oldValue: 0 }),
expect.any(Object),
null,
)
expect(vCustom.unmounted).toHaveBeenCalledWith(
el,
expect.objectContaining({ value: 1, oldValue: 0 }),
expect.any(Object),
null,
)
})
test('warn on directive with non-element root vapor child', () => {
const calls: string[] = []
const vCustom = {
created: () => calls.push('created'),
beforeMount: () => calls.push('beforeMount'),
mounted: () => calls.push('mounted'),
beforeUpdate: () => calls.push('beforeUpdate'),
updated: () => calls.push('updated'),
beforeUnmount: () => calls.push('beforeUnmount'),
unmounted: () => calls.push('unmounted'),
}
const VaporChild = defineVaporComponent({
setup() {
return [template('')(), template('')()]
},
})
const App = defineComponent({
setup() {
return () =>
h('div', null, [withDirectives(h(VaporChild as any), [[vCustom]])])
},
})
const root = document.createElement('div')
const app = createApp(App)
app.use(vaporInteropPlugin)
app.mount(root)
if (__DEV__) {
expect(
`Runtime directive used on component with non-element root node.`,
).toHaveBeenWarned()
}
expect(calls.length).toBe(0)
app.unmount()
})
})
describe('slots', () => {
test('basic', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => renderSlot(slots, 'default')
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: () => document.createTextNode('default slot'),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('default slot')
})
test('functional slot', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => createVNode(slots.default!)
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: () => document.createTextNode('default slot'),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('default slot')
})
test('slots.default() direct invocation', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => h('div', null, slots.default!())
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: () => template('direct call slot')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('direct call slot
')
})
test('slots.default() with slot props', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => h('div', null, slots.default!({ msg: 'hello' }))
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: (props: { msg: string }) => {
const n0 = template('')()
n0.textContent = props.msg
return [n0]
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('hello
')
})
test('named slot with slots[name]() invocation', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () =>
h('div', null, [
h('header', null, slots.header!()),
h('main', null, slots.default!()),
h('footer', null, slots.footer!()),
])
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
header: () => template('Header')(),
default: () => template('Main')(),
footer: () => template('Footer')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe(
'Main
',
)
})
test('slots.default() return directly', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => slots.default!()
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: () => template('direct return slot')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('direct return slot')
})
test('rendering forwarding vapor slot', () => {
const VDomChild = defineComponent({
setup(_, { slots }) {
return () => h('div', null, { default: slots.default })
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
null,
{
default: () => template('forwarded slot')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('forwarded slot
')
})
})
describe('provide / inject', () => {
it('should inject value from vdom parent', async () => {
const VaporChild = defineVaporComponent({
setup() {
const foo = inject('foo')
const n0 = template(' ')() as any
renderEffect(() => setText(n0, toDisplayString(foo)))
return n0
},
})
const value = ref('foo')
const { html } = define({
setup() {
provide('foo', value)
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('foo')
value.value = 'bar'
await nextTick()
expect(html()).toBe('bar')
})
})
describe('template ref', () => {
it('useTemplateRef with vapor child', async () => {
const VaporChild = defineVaporComponent({
setup(_, { expose }) {
const foo = ref('foo')
expose({ foo })
const n0 = template(' ')() as any
renderEffect(() => setText(n0, toDisplayString(foo)))
return n0
},
})
let elRef: ShallowRef
const { html } = define({
setup() {
elRef = useTemplateRef('el')
return () => h(VaporChild as any, { ref: 'el' })
},
}).render()
expect(html()).toBe('foo')
elRef!.value.foo = 'bar'
await nextTick()
expect(html()).toBe('bar')
})
it('static ref with vapor child', async () => {
const VaporChild = defineVaporComponent({
setup(_, { expose }) {
const foo = ref('foo')
expose({ foo })
const n0 = template(' ')() as any
renderEffect(() => setText(n0, toDisplayString(foo)))
return n0
},
})
let elRef: ShallowRef
const { html } = define({
setup() {
elRef = shallowRef()
return { elRef }
},
render() {
return h(VaporChild as any, { ref: 'elRef' })
},
}).render()
expect(html()).toBe('foo')
elRef!.value.foo = 'bar'
await nextTick()
expect(html()).toBe('bar')
})
it('dynamic component includes vdom component', async () => {
const VdomChild = defineComponent({
setup(_, { expose }) {
expose({ name: 'vdomChild' })
return () => h('div', 'vdom child')
},
})
const VaporChild = defineVaporComponent({
setup() {
return { vdomRef }
},
render() {
const setRef = createTemplateRefSetter()
const n0 = createDynamicComponent(() => VdomChild)
setRef(n0, vdomRef, false, 'vdomRef')
return n0
},
})
const vdomRef = ref(null)
define({
setup() {
return () => h(VaporChild as any)
},
}).render()
await nextTick()
expect(vdomRef.value).toBeDefined()
expect(vdomRef.value.name).toBe('vdomChild')
})
})
describe('dynamic component', () => {
it('dynamic component with vapor child', async () => {
const VaporChild = defineVaporComponent({
setup() {
return template('vapor child
')() as any
},
})
const VdomChild = defineComponent({
setup() {
return () => h('div', 'vdom child')
},
})
const view = shallowRef(VaporChild)
const { html } = define({
setup() {
return () => h(resolveDynamicComponent(view.value) as any)
},
}).render()
expect(html()).toBe('vapor child
')
view.value = VdomChild
await nextTick()
expect(html()).toBe('vdom child
')
view.value = VaporChild
await nextTick()
expect(html()).toBe('vapor child
')
})
describe('render VNodes', () => {
it('should render VNode containing vapor component from VDOM slot', async () => {
const VaporComp = defineVaporComponent({
setup() {
return template('vapor comp
')() as any
},
})
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(VaporComp as any)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createDynamicComponent(() => slotProps.Component)
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vapor comp
')
})
it('should render VNode containing vdom component from VDOM slot', async () => {
const VdomComp = defineComponent({
setup() {
return () => h('div', 'vdom comp')
},
})
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(VdomComp)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createDynamicComponent(() => slotProps.Component)
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vdom comp
')
})
it('should update when VNode changes', async () => {
const VaporCompA = defineVaporComponent({
setup() {
return template('vapor A
')() as any
},
})
const VaporCompB = defineVaporComponent({
setup() {
return template('vapor B
')() as any
},
})
const current = shallowRef(VaporCompA)
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(current.value as any)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createDynamicComponent(() => slotProps.Component)
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vapor A
')
current.value = VaporCompB
await nextTick()
expect(html()).toBe('vapor B
')
})
describe('with VaporKeepAlive', () => {
it('switch VNode with inner vapor components', async () => {
const hooksA = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const hooksB = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const VaporCompA = defineVaporComponent({
setup() {
onMounted(() => hooksA.mounted())
onActivated(() => hooksA.activated())
onDeactivated(() => hooksA.deactivated())
onUnmounted(() => hooksA.unmounted())
return template('vapor A
')() as any
},
})
const VaporCompB = defineVaporComponent({
setup() {
onMounted(() => hooksB.mounted())
onActivated(() => hooksB.activated())
onDeactivated(() => hooksB.deactivated())
onUnmounted(() => hooksB.unmounted())
return template('vapor B
')() as any
},
})
const current = shallowRef(VaporCompA)
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(current.value as any)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createComponent(VaporKeepAlive, null, {
default: () =>
createDynamicComponent(() => slotProps.Component),
})
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vapor A
')
// A: mounted + activated
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(1)
expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
current.value = VaporCompB
await nextTick()
expect(html()).toBe('vapor B
')
// A: deactivated (cached)
expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
// B: mounted + activated
expect(hooksB.mounted).toHaveBeenCalledTimes(1)
expect(hooksB.activated).toHaveBeenCalledTimes(1)
current.value = VaporCompA
await nextTick()
expect(html()).toBe('vapor A
')
// B: deactivated (cached)
expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
// A: re-activated (not re-mounted)
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(2)
})
it('switch VNode with inner VDOM components', async () => {
const hooksA = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const hooksB = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const VDOMCompA = defineComponent({
setup() {
onMounted(() => hooksA.mounted())
onActivated(() => hooksA.activated())
onDeactivated(() => hooksA.deactivated())
onUnmounted(() => hooksA.unmounted())
return () => h('div', 'vdom A')
},
})
const VDOMCompB = defineComponent({
setup() {
onMounted(() => hooksB.mounted())
onActivated(() => hooksB.activated())
onDeactivated(() => hooksB.deactivated())
onUnmounted(() => hooksB.unmounted())
return () => h('div', 'vdom B')
},
})
const current = shallowRef(VDOMCompA)
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(current.value as any)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createComponent(VaporKeepAlive, null, {
default: () =>
createDynamicComponent(() => slotProps.Component),
})
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vdom A
')
// A: mounted + activated
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(1)
expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
current.value = VDOMCompB
await nextTick()
expect(html()).toBe('vdom B
')
// A: deactivated (cached)
expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
// B: mounted + activated
expect(hooksB.mounted).toHaveBeenCalledTimes(1)
expect(hooksB.activated).toHaveBeenCalledTimes(1)
current.value = VDOMCompA
await nextTick()
expect(html()).toBe('vdom A
')
// B: deactivated (cached)
expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
// A: re-activated (not re-mounted)
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(2)
})
it('switch VNode with inner mixed vapor/VDOM components', async () => {
const hooksA = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const hooksB = {
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
const VaporCompA = defineVaporComponent({
setup() {
onMounted(() => hooksA.mounted())
onActivated(() => hooksA.activated())
onDeactivated(() => hooksA.deactivated())
onUnmounted(() => hooksA.unmounted())
return template('vapor A
')()
},
})
const VDOMCompB = defineComponent({
setup() {
onMounted(() => hooksB.mounted())
onActivated(() => hooksB.activated())
onDeactivated(() => hooksB.deactivated())
onUnmounted(() => hooksB.unmounted())
return () => h('div', 'vdom B')
},
})
const current = shallowRef(VaporCompA)
const RouterView = defineComponent({
setup(_, { slots }) {
return () => {
const component = h(current.value as any)
return slots.default!({ Component: component })
}
},
})
const App = defineVaporComponent({
setup() {
return createComponent(
RouterView as any,
null,
{
default: (slotProps: { Component: any }) => {
return createComponent(VaporKeepAlive, null, {
default: () =>
createDynamicComponent(() => slotProps.Component),
})
},
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(App as any)
},
}).render()
expect(html()).toBe('vapor A
')
// A (vapor): mounted + activated
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(1)
expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
current.value = VDOMCompB
await nextTick()
expect(html()).toBe('vdom B
')
// A (vapor): deactivated (cached)
expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
// B (vdom): mounted + activated
expect(hooksB.mounted).toHaveBeenCalledTimes(1)
expect(hooksB.activated).toHaveBeenCalledTimes(1)
current.value = VaporCompA
await nextTick()
expect(html()).toBe('vapor A
')
// B (vdom): deactivated (cached)
expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
// A (vapor): re-activated (not re-mounted)
expect(hooksA.mounted).toHaveBeenCalledTimes(1)
expect(hooksA.activated).toHaveBeenCalledTimes(2)
})
})
})
})
describe('attribute fallthrough', () => {
it('should fallthrough attrs to vdom child', () => {
const VDomChild = defineComponent({
setup() {
return () => h('div')
},
})
const VaporChild = defineVaporComponent({
setup() {
return createComponent(
VDomChild as any,
{ foo: () => 'vapor foo' },
null,
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any, { foo: 'foo', bar: 'bar' })
},
}).render()
expect(html()).toBe('')
})
it('should not fallthrough emit handlers to vdom child', () => {
const VDomChild = defineComponent({
emits: ['click'],
setup(_, { emit }) {
return () => h('button', { onClick: () => emit('click') }, 'click me')
},
})
const fn = vi.fn()
const VaporChild = defineVaporComponent({
emits: ['click'],
setup() {
return createComponent(
VDomChild as any,
{ onClick: () => fn },
null,
true,
)
},
})
const { host, html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
expect(html()).toBe('')
const button = host.querySelector('button')!
button.dispatchEvent(new Event('click'))
// fn should be called once
expect(fn).toHaveBeenCalledTimes(1)
})
})
describe('async component', () => {
const duration = 5
test('render vapor async component', async () => {
const VdomChild = {
setup() {
return () => h('div', 'foo')
},
}
const VaporAsyncChild = defineVaporAsyncComponent({
loader: () => {
return new Promise(r => {
setTimeout(() => {
r(VdomChild as any)
}, duration)
})
},
loadingComponent: () => h('span', 'loading...'),
})
const { html } = define({
setup() {
return () => h(VaporAsyncChild as any)
},
}).render()
expect(html()).toBe('loading...')
await new Promise(r => setTimeout(r, duration))
await nextTick()
expect(html()).toBe('foo
')
})
})
describe('keepalive', () => {
function assertHookCalls(
hooks: {
beforeMount: any
mounted: any
activated: any
deactivated: any
unmounted: any
},
callCounts: number[],
) {
expect([
hooks.beforeMount.mock.calls.length,
hooks.mounted.mock.calls.length,
hooks.activated.mock.calls.length,
hooks.deactivated.mock.calls.length,
hooks.unmounted.mock.calls.length,
]).toEqual(callCounts)
}
let hooks: any
beforeEach(() => {
hooks = {
beforeMount: vi.fn(),
mounted: vi.fn(),
activated: vi.fn(),
deactivated: vi.fn(),
unmounted: vi.fn(),
}
})
test('render vapor component', async () => {
const VaporChild = defineVaporComponent({
setup() {
const msg = ref('vapor')
onBeforeMount(() => hooks.beforeMount())
onMounted(() => hooks.mounted())
onActivated(() => hooks.activated())
onDeactivated(() => hooks.deactivated())
onUnmounted(() => hooks.unmounted())
const n0 = template('', true)() as any
applyTextModel(
n0,
() => msg.value,
_value => (msg.value = _value),
)
return n0
},
})
const show = ref(true)
const toggle = ref(true)
const { html, host } = define({
setup() {
return () =>
show.value
? h(KeepAlive, null, {
default: () => (toggle.value ? h(VaporChild as any) : null),
})
: null
},
}).render()
expect(html()).toBe('')
let inputEl = host.firstChild as HTMLInputElement
expect(inputEl.value).toBe('vapor')
assertHookCalls(hooks, [1, 1, 1, 0, 0])
// change input value
inputEl.value = 'changed'
inputEl.dispatchEvent(new Event('input'))
await nextTick()
// deactivate
toggle.value = false
await nextTick()
expect(html()).toBe('')
assertHookCalls(hooks, [1, 1, 1, 1, 0])
// activate
toggle.value = true
await nextTick()
expect(html()).toBe('')
inputEl = host.firstChild as HTMLInputElement
expect(inputEl.value).toBe('changed')
assertHookCalls(hooks, [1, 1, 2, 1, 0])
// unmount keepalive
show.value = false
await nextTick()
expect(html()).toBe('')
assertHookCalls(hooks, [1, 1, 2, 2, 1])
// mount keepalive
show.value = true
await nextTick()
inputEl = host.firstChild as HTMLInputElement
expect(inputEl.value).toBe('vapor')
assertHookCalls(hooks, [2, 2, 3, 2, 1])
})
test('render vapor slot', async () => {
const show = ref(true)
const VDomComp = defineComponent({
setup(_, { slots }) {
return () => renderSlot(slots, 'default')
},
})
const App = defineVaporComponent({
setup() {
const n5 = createComponent(VaporKeepAlive, null, {
default: () =>
createIf(
() => show.value,
() =>
createComponent(VDomComp as any, null, {
default: () => template('slot text')(),
}),
),
})
return n5
},
})
const { html } = define({
setup() {
return () => h(App)
},
}).render()
expect(html()).toBe('slot text')
show.value = false
await nextTick()
expect(html()).toBe('')
show.value = true
await nextTick()
expect(html()).toBe('slot text')
})
})
describe('Teleport', () => {
test('mounts VDOM Teleport from createDynamicComponent', async () => {
const target = document.createElement('div')
target.id = 'interop-teleport-target'
document.body.appendChild(target)
try {
const VaporChild = defineVaporComponent({
setup() {
return createDynamicComponent(
() => Teleport,
{ to: () => '#interop-teleport-target' },
{
default: () => template('teleported')(),
},
true,
)
},
})
define({
setup() {
return () => h(VaporChild as any)
},
}).render()
await nextTick()
expect(target.innerHTML).toContain('teleported')
} finally {
target.remove()
}
})
})
describe('Suspense', () => {
test('renders async vapor child inside VDOM Suspense', async () => {
const duration = 5
const VaporAsyncChild = defineVaporComponent({
async setup() {
await new Promise(resolve => setTimeout(resolve, duration))
return template('')()
},
})
const VaporParent = defineVaporComponent({
setup() {
return createComponent(
Suspense as any,
null,
{
default: () => createComponent(VaporAsyncChild, null, null, true),
fallback: () => template('loading')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporParent as any)
},
}).render()
expect(html()).toContain('loading')
await new Promise(resolve => setTimeout(resolve, duration + 1))
await nextTick()
expect(html()).toContain('')
})
test('renders async VDOM child inside VDOM Suspense', async () => {
const duration = 5
const VDomAsyncChild = defineComponent({
async setup() {
await new Promise(resolve => setTimeout(resolve, duration))
return () => h('div', [h('button', 'click')])
},
})
const VaporParent = defineVaporComponent({
setup() {
return createComponent(
Suspense as any,
null,
{
default: () =>
createComponent(VDomAsyncChild as any, null, null, true),
fallback: () => template('loading')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporParent as any)
},
}).render()
expect(html()).toContain('loading')
await new Promise(resolve => setTimeout(resolve, duration + 1))
await nextTick()
expect(html()).toContain('')
})
test('renders async VDOM child from vapor slot outlet inside VDOM Suspense', async () => {
const duration = 5
const VaporSlotOutlet = defineVaporComponent({
setup() {
return createSlot('default')
},
})
const VDomAsyncChild = defineComponent({
async setup() {
await new Promise(resolve => setTimeout(resolve, duration))
return () => h('div', 'slot async')
},
})
const App = defineComponent({
setup() {
return () =>
h(Suspense, null, {
default: () =>
h(VaporSlotOutlet as any, null, {
default: () => [h(VDomAsyncChild as any)],
}),
fallback: () => h('div', 'loading'),
})
},
})
const { html } = define(App).render()
expect(html()).toContain('loading')
await new Promise(resolve => setTimeout(resolve, duration + 1))
await nextTick()
expect(html()).toContain('slot async
')
})
test('renders async VDOM vnode via createDynamicComponent inside VDOM Suspense', async () => {
const duration = 5
const VDomAsyncChild = defineComponent({
async setup() {
await new Promise(resolve => setTimeout(resolve, duration))
return () => h('button', 'vnode async')
},
})
const VaporParent = defineVaporComponent({
setup() {
return createComponent(
Suspense as any,
null,
{
default: () =>
createDynamicComponent(
() => h(VDomAsyncChild as any),
null,
null,
true,
),
fallback: () => template('loading')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporParent as any)
},
}).render()
expect(html()).toContain('loading')
await new Promise(resolve => setTimeout(resolve, duration + 1))
await nextTick()
expect(html()).toContain('')
})
test('mounts VDOM Suspense from createDynamicComponent', async () => {
const VaporChild = defineVaporComponent({
setup() {
return createDynamicComponent(
() => Suspense,
null,
{
default: () => template('resolved')(),
fallback: () => template('fallback')(),
},
true,
)
},
})
const { html } = define({
setup() {
return () => h(VaporChild as any)
},
}).render()
await nextTick()
expect(html()).toContain('resolved')
})
})
})