| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689 |
- 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 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<any>(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('<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-->')
- })
- })
- 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>')
- })
- })
- })
|