| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757 |
- 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('<div class="foo bar"></div>')
- })
- })
- 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('<h1> </h1>')() as any
- const n1 = template('<input>')() 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('<h1>foo</h1><input>')
- const inputEl = host.querySelector('input')!
- inputEl.value = 'bar'
- inputEl.dispatchEvent(new Event('input'))
- await nextTick()
- expect(html()).toBe('<h1>bar</h1><input>')
- })
- 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 <slot/>
- 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('<input>')() 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('<div style="display: none;"></div>')
- show.value = true
- await nextTick()
- expect(html()).toBe('<div style=""></div>')
- })
- test('apply v-show to vapor child', async () => {
- const VaporChild = defineVaporComponent({
- setup() {
- return template('<div></div>', 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(
- '<div><div style="display: none;"></div></div>',
- )
- show.value = true
- await nextTick()
- expect(root.innerHTML).toBe('<div><div style=""></div></div>')
- })
- 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('<div></div>', 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('<div></div>')(), template('<div></div>')()]
- },
- })
- 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('<div>direct call slot</div>')
- })
- 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('<span></span>')()
- n0.textContent = props.msg
- return [n0]
- },
- },
- true,
- )
- },
- })
- const { html } = define({
- setup() {
- return () => h(VaporChild as any)
- },
- }).render()
- expect(html()).toBe('<div><span>hello</span></div>')
- })
- 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(
- '<div><header>Header</header><main>Main</main><footer>Footer</footer></div>',
- )
- })
- 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('<div>forwarded slot</div>')
- })
- })
- 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 vdomRef = ref<any>(null)
- 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
- },
- })
- define({
- setup() {
- return () => h(VaporChild as any)
- },
- }).render()
- await nextTick()
- expect(vdomRef.value).toBeDefined()
- expect(vdomRef.value.name).toBe('vdomChild')
- })
- it('dynamic component includes vdom component should cleanup old ref', async () => {
- const VdomChild = defineComponent({
- setup(_, { expose }) {
- expose({ name: 'vdomChild' })
- return () => h('div', 'vdom child')
- },
- })
- const useA = ref(true)
- const refA = ref<any>(null)
- const refB = ref<any>(null)
- const VaporChild = defineVaporComponent({
- setup() {
- const setRef = createTemplateRefSetter()
- const n0 = createDynamicComponent(() => VdomChild)
- renderEffect(() => {
- setRef(n0, useA.value ? refA : refB, false, 'vdomRef')
- })
- return n0
- },
- })
- define({
- setup() {
- return () => h(VaporChild as any)
- },
- }).render()
- await nextTick()
- expect(refA.value).toBeDefined()
- expect(refA.value.name).toBe('vdomChild')
- expect(refB.value).toBe(null)
- useA.value = false
- await nextTick()
- expect(refA.value).toBe(null)
- expect(refB.value).toBeDefined()
- expect(refB.value.name).toBe('vdomChild')
- })
- })
- describe('dynamic component', () => {
- it('dynamic component with vapor child', async () => {
- const VaporChild = defineVaporComponent({
- setup() {
- return template('<div>vapor child</div>')() as any
- },
- })
- const VdomChild = defineComponent({
- setup() {
- return () => h('div', 'vdom child')
- },
- })
- const view = shallowRef<any>(VaporChild)
- const { html } = define({
- setup() {
- return () => h(resolveDynamicComponent(view.value) as any)
- },
- }).render()
- expect(html()).toBe('<div>vapor child</div>')
- view.value = VdomChild
- await nextTick()
- expect(html()).toBe('<div>vdom child</div>')
- view.value = VaporChild
- await nextTick()
- expect(html()).toBe('<div>vapor child</div>')
- })
- describe('render VNodes', () => {
- it('should render VNode containing vapor component from VDOM slot', async () => {
- const VaporComp = defineVaporComponent({
- setup() {
- return template('<div>vapor comp</div>')() 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('<div>vapor comp</div><!--dynamic-component-->')
- })
- 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('<div>vdom comp</div><!--dynamic-component-->')
- })
- it('should update when VNode changes', async () => {
- const VaporCompA = defineVaporComponent({
- setup() {
- return template('<div>vapor A</div>')() as any
- },
- })
- const VaporCompB = defineVaporComponent({
- setup() {
- return template('<div>vapor B</div>')() as any
- },
- })
- const current = shallowRef<any>(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('<div>vapor A</div><!--dynamic-component-->')
- current.value = VaporCompB
- await nextTick()
- expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
- })
- 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('<div>vapor A</div>')() as any
- },
- })
- const VaporCompB = defineVaporComponent({
- setup() {
- onMounted(() => hooksB.mounted())
- onActivated(() => hooksB.activated())
- onDeactivated(() => hooksB.deactivated())
- onUnmounted(() => hooksB.unmounted())
- return template('<div>vapor B</div>')() as any
- },
- })
- const current = shallowRef<any>(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('<div>vapor A</div><!--dynamic-component-->')
- // 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('<div>vapor B</div><!--dynamic-component-->')
- // 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('<div>vapor A</div><!--dynamic-component-->')
- // 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<any>(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('<div>vdom A</div><!--dynamic-component-->')
- // 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('<div>vdom B</div><!--dynamic-component-->')
- // 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('<div>vdom A</div><!--dynamic-component-->')
- // 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('<div>vapor A</div>')()
- },
- })
- const VDOMCompB = defineComponent({
- setup() {
- onMounted(() => hooksB.mounted())
- onActivated(() => hooksB.activated())
- onDeactivated(() => hooksB.deactivated())
- onUnmounted(() => hooksB.unmounted())
- return () => h('div', 'vdom B')
- },
- })
- const current = shallowRef<any>(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('<div>vapor A</div><!--dynamic-component-->')
- // 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('<div>vdom B</div><!--dynamic-component-->')
- // 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('<div>vapor A</div><!--dynamic-component-->')
- // 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('<div foo="foo" bar="bar"></div>')
- })
- 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('<button>click me</button>')
- 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('<span>loading...</span><!--async component-->')
- await new Promise(r => setTimeout(r, duration))
- await nextTick()
- expect(html()).toBe('<div>foo</div><!--async component-->')
- })
- })
- 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('<input type="text">', 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('<input type="text">')
- 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('<input type="text">')
- 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<!--if-->')
- show.value = false
- await nextTick()
- expect(html()).toBe('<!--if-->')
- show.value = true
- await nextTick()
- expect(html()).toBe('slot text<!--if-->')
- })
- test('unmounting vapor slot should remove vnode slot content', async () => {
- const show = ref(true)
- const VaporSlotOutlet = defineVaporComponent({
- setup() {
- return createSlot('default')
- },
- })
- const { html } = define({
- setup() {
- return () =>
- h('div', null, [
- show.value
- ? h(VaporSlotOutlet as any, null, {
- default: () => [h('span', 'slot vnode')],
- })
- : null,
- ])
- },
- }).render()
- expect(html()).toBe('<div><span>slot vnode</span></div>')
- show.value = false
- await nextTick()
- expect(html()).toBe('<div><!----></div>')
- })
- })
- 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('<span>teleported</span>')(),
- },
- true,
- )
- },
- })
- define({
- setup() {
- return () => h(VaporChild as any)
- },
- }).render()
- await nextTick()
- expect(target.innerHTML).toContain('<span>teleported</span>')
- } 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('<div><button>click</button></div>')()
- },
- })
- 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('<div><button>click</button></div>')
- })
- 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('<div><button>click</button></div>')
- })
- 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('<div>slot async</div>')
- })
- 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('<button>vnode async</button>')
- })
- test('mounts VDOM Suspense from createDynamicComponent', async () => {
- const VaporChild = defineVaporComponent({
- setup() {
- return createDynamicComponent(
- () => Suspense,
- null,
- {
- default: () => template('<span>resolved</span>')(),
- fallback: () => template('<span>fallback</span>')(),
- },
- true,
- )
- },
- })
- const { html } = define({
- setup() {
- return () => h(VaporChild as any)
- },
- }).render()
- await nextTick()
- expect(html()).toContain('<span>resolved</span>')
- })
- })
- })
|