if.spec.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {
  2. child,
  3. createIf,
  4. insert,
  5. renderEffect,
  6. template,
  7. // @ts-expect-error
  8. withDirectives,
  9. } from '../src'
  10. import { nextTick, ref } from '@vue/runtime-dom'
  11. import type { Mock } from 'vitest'
  12. import { makeRender } from './_utils'
  13. import { unmountComponent } from '../src/component'
  14. import { setElementText } from '../src/dom/prop'
  15. import type { DynamicFragment } from '../src/fragment'
  16. const define = makeRender()
  17. describe('createIf', () => {
  18. test('basic', async () => {
  19. // mock this template:
  20. // <div>
  21. // <p v-if="counter">{{counter}}</p>
  22. // <p v-else>zero</p>
  23. // </div>
  24. let spyIfFn: Mock<() => Node>
  25. let spyElseFn: Mock<() => Node>
  26. const count = ref(0)
  27. const spyConditionFn = vi.fn(() => count.value)
  28. // templates can be reused through caching.
  29. const t0 = template('<div></div>')
  30. const t1 = template('<p></p>')
  31. const t2 = template('<p>zero</p>')
  32. const { host } = define(() => {
  33. const n0 = t0()
  34. insert(
  35. createIf(
  36. spyConditionFn,
  37. // v-if
  38. (spyIfFn ||= vi.fn(() => {
  39. const n2 = t1()
  40. renderEffect(() => {
  41. setElementText(n2, count.value)
  42. })
  43. return n2
  44. })),
  45. // v-else
  46. (spyElseFn ||= vi.fn(() => {
  47. const n4 = t2()
  48. return n4
  49. })),
  50. ),
  51. n0 as any as ParentNode,
  52. )
  53. return n0
  54. }).render()
  55. expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
  56. expect(spyConditionFn).toHaveBeenCalledTimes(1)
  57. expect(spyIfFn!).toHaveBeenCalledTimes(0)
  58. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  59. count.value++
  60. await nextTick()
  61. expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
  62. expect(spyConditionFn).toHaveBeenCalledTimes(2)
  63. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  64. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  65. count.value++
  66. await nextTick()
  67. expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
  68. expect(spyConditionFn).toHaveBeenCalledTimes(3)
  69. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  70. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  71. count.value = 0
  72. await nextTick()
  73. expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
  74. expect(spyConditionFn).toHaveBeenCalledTimes(4)
  75. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  76. expect(spyElseFn!).toHaveBeenCalledTimes(2)
  77. })
  78. test('should handle nested template', async () => {
  79. // mock this template:
  80. // <template v-if="ok1">
  81. // Hello <template v-if="ok2">Vapor</template>
  82. // </template>
  83. const ok1 = ref(true)
  84. const ok2 = ref(true)
  85. const t0 = template('Vapor')
  86. const t1 = template('Hello ')
  87. const { host } = define(() => {
  88. const n1 = createIf(
  89. () => ok1.value,
  90. () => {
  91. const n2 = t1()
  92. const n3 = createIf(
  93. () => ok2.value,
  94. () => {
  95. const n4 = t0()
  96. return n4
  97. },
  98. )
  99. return [n2, n3]
  100. },
  101. )
  102. return n1
  103. }).render()
  104. expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
  105. ok1.value = false
  106. await nextTick()
  107. expect(host.innerHTML).toBe('<!--if-->')
  108. ok1.value = true
  109. await nextTick()
  110. expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
  111. ok2.value = false
  112. await nextTick()
  113. expect(host.innerHTML).toBe('Hello <!--if--><!--if-->')
  114. ok1.value = false
  115. await nextTick()
  116. expect(host.innerHTML).toBe('<!--if-->')
  117. })
  118. test('with v-once', async () => {
  119. const toggle = ref(false)
  120. const { html } = define({
  121. setup() {
  122. return createIf(
  123. () => toggle.value,
  124. () => template('<p>foo</p>')(),
  125. () => template('<p>bar</p>')(),
  126. undefined,
  127. true,
  128. )
  129. },
  130. }).render()
  131. expect(html()).toBe('<p>bar</p>')
  132. toggle.value = true
  133. await nextTick()
  134. // should not change
  135. expect(html()).toBe('<p>bar</p>')
  136. })
  137. test('should trigger fragment onUpdated when branch becomes empty', async () => {
  138. const show = ref(true)
  139. const onUpdated = vi.fn()
  140. let frag!: DynamicFragment
  141. const { host } = define(() => {
  142. frag = createIf(
  143. () => show.value,
  144. () => template('<div>foo</div>')(),
  145. ) as DynamicFragment
  146. frag.onUpdated = [onUpdated]
  147. return frag
  148. }).render()
  149. expect(host.innerHTML).toBe('<div>foo</div><!--if-->')
  150. show.value = false
  151. await nextTick()
  152. expect(host.innerHTML).toBe('<!--if-->')
  153. expect(onUpdated).toHaveBeenCalledTimes(1)
  154. expect(onUpdated).toHaveBeenLastCalledWith([])
  155. show.value = true
  156. await nextTick()
  157. expect(host.innerHTML).toBe('<div>foo</div><!--if-->')
  158. expect(onUpdated).toHaveBeenCalledTimes(2)
  159. })
  160. // vapor custom directives have no lifecycle hooks.
  161. test.todo('should work with directive hooks', async () => {
  162. const calls: string[] = []
  163. const show1 = ref(true)
  164. const show2 = ref(true)
  165. const update = ref(0)
  166. const spyConditionFn1 = vi.fn(() => show1.value)
  167. const spyConditionFn2 = vi.fn(() => show2.value)
  168. const vDirective: any = {
  169. created: (el: any, { value }: any) => calls.push(`${value} created`),
  170. beforeMount: (el: any, { value }: any) =>
  171. calls.push(`${value} beforeMount`),
  172. mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
  173. beforeUpdate: (el: any, { value }: any) =>
  174. calls.push(`${value} beforeUpdate`),
  175. updated: (el: any, { value }: any) => calls.push(`${value} updated`),
  176. beforeUnmount: (el: any, { value }: any) =>
  177. calls.push(`${value} beforeUnmount`),
  178. unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
  179. }
  180. const t0 = template('<p></p>')
  181. const { instance } = define(() => {
  182. const n1 = createIf(
  183. spyConditionFn1,
  184. () => {
  185. const n2 = t0() as ParentNode
  186. withDirectives(child(n2), [[vDirective, () => (update.value, '1')]])
  187. return n2
  188. },
  189. () =>
  190. createIf(
  191. spyConditionFn2,
  192. () => {
  193. const n2 = t0() as ParentNode
  194. withDirectives(child(n2), [[vDirective, () => '2']])
  195. return n2
  196. },
  197. () => {
  198. const n2 = t0() as ParentNode
  199. withDirectives(child(n2), [[vDirective, () => '3']])
  200. return n2
  201. },
  202. ),
  203. )
  204. return [n1]
  205. }).render()
  206. await nextTick()
  207. expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
  208. calls.length = 0
  209. expect(spyConditionFn1).toHaveBeenCalledTimes(1)
  210. expect(spyConditionFn2).toHaveBeenCalledTimes(0)
  211. show1.value = false
  212. await nextTick()
  213. expect(calls).toEqual([
  214. '1 beforeUnmount',
  215. '2 created',
  216. '2 beforeMount',
  217. '1 unmounted',
  218. '2 mounted',
  219. ])
  220. calls.length = 0
  221. expect(spyConditionFn1).toHaveBeenCalledTimes(2)
  222. expect(spyConditionFn2).toHaveBeenCalledTimes(1)
  223. show2.value = false
  224. await nextTick()
  225. expect(calls).toEqual([
  226. '2 beforeUnmount',
  227. '3 created',
  228. '3 beforeMount',
  229. '2 unmounted',
  230. '3 mounted',
  231. ])
  232. calls.length = 0
  233. expect(spyConditionFn1).toHaveBeenCalledTimes(2)
  234. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  235. show1.value = true
  236. await nextTick()
  237. expect(calls).toEqual([
  238. '3 beforeUnmount',
  239. '1 created',
  240. '1 beforeMount',
  241. '3 unmounted',
  242. '1 mounted',
  243. ])
  244. calls.length = 0
  245. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  246. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  247. update.value++
  248. await nextTick()
  249. expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
  250. calls.length = 0
  251. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  252. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  253. unmountComponent(instance!)
  254. expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
  255. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  256. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  257. })
  258. })