| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- import {
- KeepAlive,
- type ShallowRef,
- createVNode,
- defineComponent,
- h,
- inject,
- nextTick,
- onActivated,
- onBeforeMount,
- onDeactivated,
- onMounted,
- onUnmounted,
- provide,
- ref,
- renderSlot,
- resolveDynamicComponent,
- shallowRef,
- toDisplayString,
- useModel,
- useTemplateRef,
- } from '@vue/runtime-dom'
- import { makeInteropRender } from './_utils'
- import {
- applyTextModel,
- applyVShow,
- child,
- createComponent,
- defineVaporAsyncComponent,
- defineVaporComponent,
- renderEffect,
- setText,
- template,
- } 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>')
- })
- })
- 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('v-show', () => {
- 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>')
- })
- })
- 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')
- })
- })
- 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')
- })
- })
- 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('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])
- })
- })
- })
|