vdomInterop.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import {
  2. KeepAlive,
  3. createVNode,
  4. defineComponent,
  5. h,
  6. nextTick,
  7. onActivated,
  8. onBeforeMount,
  9. onDeactivated,
  10. onMounted,
  11. onUnmounted,
  12. ref,
  13. renderSlot,
  14. toDisplayString,
  15. useModel,
  16. } from '@vue/runtime-dom'
  17. import { makeInteropRender } from './_utils'
  18. import {
  19. applyTextModel,
  20. applyVShow,
  21. child,
  22. createComponent,
  23. defineVaporAsyncComponent,
  24. defineVaporComponent,
  25. renderEffect,
  26. setText,
  27. template,
  28. } from '../src'
  29. const define = makeInteropRender()
  30. describe('vdomInterop', () => {
  31. describe('props', () => {
  32. test('should work if props are not provided', () => {
  33. const VaporChild = defineVaporComponent({
  34. props: {
  35. msg: String,
  36. },
  37. setup(_, { attrs }) {
  38. return [document.createTextNode(attrs.class || 'foo')]
  39. },
  40. })
  41. const { html } = define({
  42. setup() {
  43. return () => h(VaporChild as any)
  44. },
  45. }).render()
  46. expect(html()).toBe('foo')
  47. })
  48. test('should handle class prop when vapor renders vdom component', () => {
  49. const VDomChild = defineComponent({
  50. setup() {
  51. return () => h('div', { class: 'foo' })
  52. },
  53. })
  54. const VaporChild = defineVaporComponent({
  55. setup() {
  56. return createComponent(VDomChild as any, { class: () => 'bar' })
  57. },
  58. })
  59. const { html } = define({
  60. setup() {
  61. return () => h(VaporChild as any)
  62. },
  63. }).render()
  64. expect(html()).toBe('<div class="foo bar"></div>')
  65. })
  66. })
  67. describe('v-model', () => {
  68. test('basic work', async () => {
  69. const VaporChild = defineVaporComponent({
  70. props: {
  71. modelValue: {},
  72. modelModifiers: {},
  73. },
  74. emits: ['update:modelValue'],
  75. setup(__props) {
  76. const modelValue = useModel(__props, 'modelValue')
  77. const n0 = template('<h1> </h1>')() as any
  78. const n1 = template('<input>')() as any
  79. const x0 = child(n0) as any
  80. applyTextModel(
  81. n1,
  82. () => modelValue.value,
  83. _value => (modelValue.value = _value),
  84. )
  85. renderEffect(() => setText(x0, toDisplayString(modelValue.value)))
  86. return [n0, n1]
  87. },
  88. })
  89. const { html, host } = define({
  90. setup() {
  91. const msg = ref('foo')
  92. return () =>
  93. h(VaporChild as any, {
  94. modelValue: msg.value,
  95. 'onUpdate:modelValue': (value: string) => {
  96. msg.value = value
  97. },
  98. })
  99. },
  100. }).render()
  101. expect(html()).toBe('<h1>foo</h1><input>')
  102. const inputEl = host.querySelector('input')!
  103. inputEl.value = 'bar'
  104. inputEl.dispatchEvent(new Event('input'))
  105. await nextTick()
  106. expect(html()).toBe('<h1>bar</h1><input>')
  107. })
  108. })
  109. describe('emit', () => {
  110. test('emit from vapor child to vdom parent', () => {
  111. const VaporChild = defineVaporComponent({
  112. emits: ['click'],
  113. setup(_, { emit }) {
  114. emit('click')
  115. return []
  116. },
  117. })
  118. const fn = vi.fn()
  119. define({
  120. setup() {
  121. return () => h(VaporChild as any, { onClick: fn })
  122. },
  123. }).render()
  124. // fn should be called once
  125. expect(fn).toHaveBeenCalledTimes(1)
  126. })
  127. })
  128. describe('v-show', () => {
  129. test('apply v-show to vdom child', async () => {
  130. const VDomChild = {
  131. setup() {
  132. return () => h('div')
  133. },
  134. }
  135. const show = ref(false)
  136. const VaporChild = defineVaporComponent({
  137. setup() {
  138. const n1 = createComponent(VDomChild as any)
  139. applyVShow(n1, () => show.value)
  140. return n1
  141. },
  142. })
  143. const { html } = define({
  144. setup() {
  145. return () => h(VaporChild as any)
  146. },
  147. }).render()
  148. expect(html()).toBe('<div style="display: none;"></div>')
  149. show.value = true
  150. await nextTick()
  151. expect(html()).toBe('<div style=""></div>')
  152. })
  153. })
  154. describe('slots', () => {
  155. test('basic', () => {
  156. const VDomChild = defineComponent({
  157. setup(_, { slots }) {
  158. return () => renderSlot(slots, 'default')
  159. },
  160. })
  161. const VaporChild = defineVaporComponent({
  162. setup() {
  163. return createComponent(
  164. VDomChild as any,
  165. null,
  166. {
  167. default: () => document.createTextNode('default slot'),
  168. },
  169. true,
  170. )
  171. },
  172. })
  173. const { html } = define({
  174. setup() {
  175. return () => h(VaporChild as any)
  176. },
  177. }).render()
  178. expect(html()).toBe('default slot')
  179. })
  180. test('functional slot', () => {
  181. const VDomChild = defineComponent({
  182. setup(_, { slots }) {
  183. return () => createVNode(slots.default!)
  184. },
  185. })
  186. const VaporChild = defineVaporComponent({
  187. setup() {
  188. return createComponent(
  189. VDomChild as any,
  190. null,
  191. {
  192. default: () => document.createTextNode('default slot'),
  193. },
  194. true,
  195. )
  196. },
  197. })
  198. const { html } = define({
  199. setup() {
  200. return () => h(VaporChild as any)
  201. },
  202. }).render()
  203. expect(html()).toBe('default slot')
  204. })
  205. })
  206. describe.todo('provide', () => {})
  207. describe.todo('inject', () => {})
  208. describe.todo('template ref', () => {})
  209. describe.todo('dynamic component', () => {})
  210. describe('attribute fallthrough', () => {
  211. it('should fallthrough attrs to vdom child', () => {
  212. const VDomChild = defineComponent({
  213. setup() {
  214. return () => h('div')
  215. },
  216. })
  217. const VaporChild = defineVaporComponent({
  218. setup() {
  219. return createComponent(
  220. VDomChild as any,
  221. { foo: () => 'vapor foo' },
  222. null,
  223. true,
  224. )
  225. },
  226. })
  227. const { html } = define({
  228. setup() {
  229. return () => h(VaporChild as any, { foo: 'foo', bar: 'bar' })
  230. },
  231. }).render()
  232. expect(html()).toBe('<div foo="foo" bar="bar"></div>')
  233. })
  234. it('should not fallthrough emit handlers to vdom child', () => {
  235. const VDomChild = defineComponent({
  236. emits: ['click'],
  237. setup(_, { emit }) {
  238. return () => h('button', { onClick: () => emit('click') }, 'click me')
  239. },
  240. })
  241. const fn = vi.fn()
  242. const VaporChild = defineVaporComponent({
  243. emits: ['click'],
  244. setup() {
  245. return createComponent(
  246. VDomChild as any,
  247. { onClick: () => fn },
  248. null,
  249. true,
  250. )
  251. },
  252. })
  253. const { host, html } = define({
  254. setup() {
  255. return () => h(VaporChild as any)
  256. },
  257. }).render()
  258. expect(html()).toBe('<button>click me</button>')
  259. const button = host.querySelector('button')!
  260. button.dispatchEvent(new Event('click'))
  261. // fn should be called once
  262. expect(fn).toHaveBeenCalledTimes(1)
  263. })
  264. })
  265. describe('async component', () => {
  266. const duration = 5
  267. test('render vapor async component', async () => {
  268. const VdomChild = {
  269. setup() {
  270. return () => h('div', 'foo')
  271. },
  272. }
  273. const VaporAsyncChild = defineVaporAsyncComponent({
  274. loader: () => {
  275. return new Promise(r => {
  276. setTimeout(() => {
  277. r(VdomChild as any)
  278. }, duration)
  279. })
  280. },
  281. loadingComponent: () => h('span', 'loading...'),
  282. })
  283. const { html } = define({
  284. setup() {
  285. return () => h(VaporAsyncChild as any)
  286. },
  287. }).render()
  288. expect(html()).toBe('<span>loading...</span><!--async component-->')
  289. await new Promise(r => setTimeout(r, duration))
  290. await nextTick()
  291. expect(html()).toBe('<div>foo</div><!--async component-->')
  292. })
  293. })
  294. describe('keepalive', () => {
  295. function assertHookCalls(
  296. hooks: {
  297. beforeMount: any
  298. mounted: any
  299. activated: any
  300. deactivated: any
  301. unmounted: any
  302. },
  303. callCounts: number[],
  304. ) {
  305. expect([
  306. hooks.beforeMount.mock.calls.length,
  307. hooks.mounted.mock.calls.length,
  308. hooks.activated.mock.calls.length,
  309. hooks.deactivated.mock.calls.length,
  310. hooks.unmounted.mock.calls.length,
  311. ]).toEqual(callCounts)
  312. }
  313. let hooks: any
  314. beforeEach(() => {
  315. hooks = {
  316. beforeMount: vi.fn(),
  317. mounted: vi.fn(),
  318. activated: vi.fn(),
  319. deactivated: vi.fn(),
  320. unmounted: vi.fn(),
  321. }
  322. })
  323. test('render vapor component', async () => {
  324. const VaporChild = defineVaporComponent({
  325. setup() {
  326. const msg = ref('vapor')
  327. onBeforeMount(() => hooks.beforeMount())
  328. onMounted(() => hooks.mounted())
  329. onActivated(() => hooks.activated())
  330. onDeactivated(() => hooks.deactivated())
  331. onUnmounted(() => hooks.unmounted())
  332. const n0 = template('<input type="text">', true)() as any
  333. applyTextModel(
  334. n0,
  335. () => msg.value,
  336. _value => (msg.value = _value),
  337. )
  338. return n0
  339. },
  340. })
  341. const show = ref(true)
  342. const toggle = ref(true)
  343. const { html, host } = define({
  344. setup() {
  345. return () =>
  346. show.value
  347. ? h(KeepAlive, null, {
  348. default: () => (toggle.value ? h(VaporChild as any) : null),
  349. })
  350. : null
  351. },
  352. }).render()
  353. expect(html()).toBe('<input type="text">')
  354. let inputEl = host.firstChild as HTMLInputElement
  355. expect(inputEl.value).toBe('vapor')
  356. assertHookCalls(hooks, [1, 1, 1, 0, 0])
  357. // change input value
  358. inputEl.value = 'changed'
  359. inputEl.dispatchEvent(new Event('input'))
  360. await nextTick()
  361. // deactivate
  362. toggle.value = false
  363. await nextTick()
  364. expect(html()).toBe('<!---->')
  365. assertHookCalls(hooks, [1, 1, 1, 1, 0])
  366. // activate
  367. toggle.value = true
  368. await nextTick()
  369. expect(html()).toBe('<input type="text">')
  370. inputEl = host.firstChild as HTMLInputElement
  371. expect(inputEl.value).toBe('changed')
  372. assertHookCalls(hooks, [1, 1, 2, 1, 0])
  373. // unmount keepalive
  374. show.value = false
  375. await nextTick()
  376. expect(html()).toBe('<!---->')
  377. assertHookCalls(hooks, [1, 1, 2, 2, 1])
  378. // mount keepalive
  379. show.value = true
  380. await nextTick()
  381. inputEl = host.firstChild as HTMLInputElement
  382. expect(inputEl.value).toBe('vapor')
  383. assertHookCalls(hooks, [2, 2, 3, 2, 1])
  384. })
  385. })
  386. })