if.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import {
  2. VaporTransition,
  3. child,
  4. createComponent,
  5. createIf,
  6. insert,
  7. renderEffect,
  8. template,
  9. // @ts-expect-error
  10. withDirectives,
  11. } from '../src'
  12. import { nextTick, ref } from '@vue/runtime-dom'
  13. import type { Mock } from 'vitest'
  14. import { makeRender } from './_utils'
  15. import { unmountComponent } from '../src/component'
  16. import { setElementText } from '../src/dom/prop'
  17. import type { DynamicFragment } from '../src/fragment'
  18. const define = makeRender()
  19. describe('createIf', () => {
  20. test('basic', async () => {
  21. // mock this template:
  22. // <div>
  23. // <p v-if="counter">{{counter}}</p>
  24. // <p v-else>zero</p>
  25. // </div>
  26. let spyIfFn: Mock<() => Node>
  27. let spyElseFn: Mock<() => Node>
  28. const count = ref(0)
  29. const spyConditionFn = vi.fn(() => count.value)
  30. // templates can be reused through caching.
  31. const t0 = template('<div></div>')
  32. const t1 = template('<p></p>')
  33. const t2 = template('<p>zero</p>')
  34. const { host } = define(() => {
  35. const n0 = t0()
  36. insert(
  37. createIf(
  38. spyConditionFn,
  39. // v-if
  40. (spyIfFn ||= vi.fn(() => {
  41. const n2 = t1()
  42. renderEffect(() => {
  43. setElementText(n2, count.value)
  44. })
  45. return n2
  46. })),
  47. // v-else
  48. (spyElseFn ||= vi.fn(() => {
  49. const n4 = t2()
  50. return n4
  51. })),
  52. ),
  53. n0 as any as ParentNode,
  54. )
  55. return n0
  56. }).render()
  57. expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
  58. expect(spyConditionFn).toHaveBeenCalledTimes(1)
  59. expect(spyIfFn!).toHaveBeenCalledTimes(0)
  60. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  61. count.value++
  62. await nextTick()
  63. expect(host.innerHTML).toBe('<div><p>1</p><!--if--></div>')
  64. expect(spyConditionFn).toHaveBeenCalledTimes(2)
  65. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  66. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  67. count.value++
  68. await nextTick()
  69. expect(host.innerHTML).toBe('<div><p>2</p><!--if--></div>')
  70. expect(spyConditionFn).toHaveBeenCalledTimes(3)
  71. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  72. expect(spyElseFn!).toHaveBeenCalledTimes(1)
  73. count.value = 0
  74. await nextTick()
  75. expect(host.innerHTML).toBe('<div><p>zero</p><!--if--></div>')
  76. expect(spyConditionFn).toHaveBeenCalledTimes(4)
  77. expect(spyIfFn!).toHaveBeenCalledTimes(1)
  78. expect(spyElseFn!).toHaveBeenCalledTimes(2)
  79. })
  80. test('should handle nested template', async () => {
  81. // mock this template:
  82. // <template v-if="ok1">
  83. // Hello <template v-if="ok2">Vapor</template>
  84. // </template>
  85. const ok1 = ref(true)
  86. const ok2 = ref(true)
  87. const t0 = template('Vapor')
  88. const t1 = template('Hello ')
  89. const { host } = define(() => {
  90. const n1 = createIf(
  91. () => ok1.value,
  92. () => {
  93. const n2 = t1()
  94. const n3 = createIf(
  95. () => ok2.value,
  96. () => {
  97. const n4 = t0()
  98. return n4
  99. },
  100. )
  101. return [n2, n3]
  102. },
  103. )
  104. return n1
  105. }).render()
  106. expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
  107. ok1.value = false
  108. await nextTick()
  109. expect(host.innerHTML).toBe('<!--if-->')
  110. ok1.value = true
  111. await nextTick()
  112. expect(host.innerHTML).toBe('Hello Vapor<!--if--><!--if-->')
  113. ok2.value = false
  114. await nextTick()
  115. expect(host.innerHTML).toBe('Hello <!--if--><!--if-->')
  116. ok1.value = false
  117. await nextTick()
  118. expect(host.innerHTML).toBe('<!--if-->')
  119. })
  120. test('with v-once', async () => {
  121. const toggle = ref(false)
  122. const { html } = define({
  123. setup() {
  124. return createIf(
  125. () => toggle.value,
  126. () => template('<p>foo</p>')(),
  127. () => template('<p>bar</p>')(),
  128. undefined,
  129. true,
  130. )
  131. },
  132. }).render()
  133. expect(html()).toBe('<p>bar</p>')
  134. toggle.value = true
  135. await nextTick()
  136. // should not change
  137. expect(html()).toBe('<p>bar</p>')
  138. })
  139. test('should trigger fragment onUpdated when branch becomes empty', async () => {
  140. const show = ref(true)
  141. const onUpdated = vi.fn()
  142. let frag!: DynamicFragment
  143. const { host } = define(() => {
  144. frag = createIf(
  145. () => show.value,
  146. () => template('<div>foo</div>')(),
  147. ) as DynamicFragment
  148. frag.onUpdated = [onUpdated]
  149. return frag
  150. }).render()
  151. expect(host.innerHTML).toBe('<div>foo</div><!--if-->')
  152. show.value = false
  153. await nextTick()
  154. expect(host.innerHTML).toBe('<!--if-->')
  155. expect(onUpdated).toHaveBeenCalledTimes(1)
  156. expect(onUpdated).toHaveBeenLastCalledWith([])
  157. show.value = true
  158. await nextTick()
  159. expect(host.innerHTML).toBe('<div>foo</div><!--if-->')
  160. expect(onUpdated).toHaveBeenCalledTimes(2)
  161. })
  162. test('should not set branch block key without Transition or KeepAlive', async () => {
  163. const show = ref(true)
  164. const t0 = template('<div>foo</div>')
  165. const t1 = template('<div>bar</div>')
  166. let branch!: any
  167. const { host } = define(() =>
  168. createIf(
  169. () => show.value,
  170. () => (branch = t0()),
  171. () => (branch = t1()),
  172. undefined,
  173. undefined,
  174. 0,
  175. ),
  176. ).render()
  177. expect(host.innerHTML).toBe('<div>foo</div><!--if-->')
  178. expect(branch.$key).toBeUndefined()
  179. show.value = false
  180. await nextTick()
  181. expect(host.innerHTML).toBe('<div>bar</div><!--if-->')
  182. expect(branch.$key).toBeUndefined()
  183. })
  184. test('should not set branch block key outside Transition after Transition is used', async () => {
  185. const show = ref(true)
  186. const transitionChild = template('<span>transition</span>')
  187. const t0 = template('<div>foo</div>')
  188. const t1 = template('<div>bar</div>')
  189. let branch!: any
  190. const { host } = define(() => [
  191. createComponent(
  192. VaporTransition,
  193. null,
  194. {
  195. default: () => transitionChild(),
  196. },
  197. true,
  198. ),
  199. createIf(
  200. () => show.value,
  201. () => (branch = t0()),
  202. () => (branch = t1()),
  203. undefined,
  204. undefined,
  205. 0,
  206. ),
  207. ]).render()
  208. expect(host.innerHTML).toBe(
  209. '<span>transition</span><div>foo</div><!--if-->',
  210. )
  211. expect(branch.$key).toBeUndefined()
  212. show.value = false
  213. await nextTick()
  214. expect(host.innerHTML).toBe(
  215. '<span>transition</span><div>bar</div><!--if-->',
  216. )
  217. expect(branch.$key).toBeUndefined()
  218. })
  219. test('should set branch block key inside Transition', () => {
  220. const show = ref(true)
  221. const t0 = template('<div>foo</div>')
  222. const t1 = template('<div>bar</div>')
  223. let branch!: any
  224. define(() =>
  225. createComponent(
  226. VaporTransition,
  227. null,
  228. {
  229. default: () =>
  230. createIf(
  231. () => show.value,
  232. () => (branch = t0()),
  233. () => (branch = t1()),
  234. undefined,
  235. undefined,
  236. 0,
  237. ),
  238. },
  239. true,
  240. ),
  241. ).render()
  242. expect(branch.$key).toBe('00')
  243. })
  244. // vapor custom directives have no lifecycle hooks.
  245. test.todo('should work with directive hooks', async () => {
  246. const calls: string[] = []
  247. const show1 = ref(true)
  248. const show2 = ref(true)
  249. const update = ref(0)
  250. const spyConditionFn1 = vi.fn(() => show1.value)
  251. const spyConditionFn2 = vi.fn(() => show2.value)
  252. const vDirective: any = {
  253. created: (el: any, { value }: any) => calls.push(`${value} created`),
  254. beforeMount: (el: any, { value }: any) =>
  255. calls.push(`${value} beforeMount`),
  256. mounted: (el: any, { value }: any) => calls.push(`${value} mounted`),
  257. beforeUpdate: (el: any, { value }: any) =>
  258. calls.push(`${value} beforeUpdate`),
  259. updated: (el: any, { value }: any) => calls.push(`${value} updated`),
  260. beforeUnmount: (el: any, { value }: any) =>
  261. calls.push(`${value} beforeUnmount`),
  262. unmounted: (el: any, { value }: any) => calls.push(`${value} unmounted`),
  263. }
  264. const t0 = template('<p></p>')
  265. const { instance } = define(() => {
  266. const n1 = createIf(
  267. spyConditionFn1,
  268. () => {
  269. const n2 = t0() as ParentNode
  270. withDirectives(child(n2), [[vDirective, () => (update.value, '1')]])
  271. return n2
  272. },
  273. () =>
  274. createIf(
  275. spyConditionFn2,
  276. () => {
  277. const n2 = t0() as ParentNode
  278. withDirectives(child(n2), [[vDirective, () => '2']])
  279. return n2
  280. },
  281. () => {
  282. const n2 = t0() as ParentNode
  283. withDirectives(child(n2), [[vDirective, () => '3']])
  284. return n2
  285. },
  286. ),
  287. )
  288. return [n1]
  289. }).render()
  290. await nextTick()
  291. expect(calls).toEqual(['1 created', '1 beforeMount', '1 mounted'])
  292. calls.length = 0
  293. expect(spyConditionFn1).toHaveBeenCalledTimes(1)
  294. expect(spyConditionFn2).toHaveBeenCalledTimes(0)
  295. show1.value = false
  296. await nextTick()
  297. expect(calls).toEqual([
  298. '1 beforeUnmount',
  299. '2 created',
  300. '2 beforeMount',
  301. '1 unmounted',
  302. '2 mounted',
  303. ])
  304. calls.length = 0
  305. expect(spyConditionFn1).toHaveBeenCalledTimes(2)
  306. expect(spyConditionFn2).toHaveBeenCalledTimes(1)
  307. show2.value = false
  308. await nextTick()
  309. expect(calls).toEqual([
  310. '2 beforeUnmount',
  311. '3 created',
  312. '3 beforeMount',
  313. '2 unmounted',
  314. '3 mounted',
  315. ])
  316. calls.length = 0
  317. expect(spyConditionFn1).toHaveBeenCalledTimes(2)
  318. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  319. show1.value = true
  320. await nextTick()
  321. expect(calls).toEqual([
  322. '3 beforeUnmount',
  323. '1 created',
  324. '1 beforeMount',
  325. '3 unmounted',
  326. '1 mounted',
  327. ])
  328. calls.length = 0
  329. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  330. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  331. update.value++
  332. await nextTick()
  333. expect(calls).toEqual(['1 beforeUpdate', '1 updated'])
  334. calls.length = 0
  335. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  336. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  337. unmountComponent(instance!)
  338. expect(calls).toEqual(['1 beforeUnmount', '1 unmounted'])
  339. expect(spyConditionFn1).toHaveBeenCalledTimes(3)
  340. expect(spyConditionFn2).toHaveBeenCalledTimes(2)
  341. })
  342. })