apiLifecycle.spec.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import { vi } from 'vitest'
  2. import {
  3. onBeforeMount,
  4. h,
  5. nodeOps,
  6. render,
  7. serializeInner,
  8. onMounted,
  9. ref,
  10. onBeforeUpdate,
  11. nextTick,
  12. onUpdated,
  13. onBeforeUnmount,
  14. onUnmounted,
  15. onRenderTracked,
  16. reactive,
  17. TrackOpTypes,
  18. onRenderTriggered
  19. } from '@vue/runtime-test'
  20. import { ITERATE_KEY, DebuggerEvent, TriggerOpTypes } from '@vue/reactivity'
  21. // reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
  22. describe('api: lifecycle hooks', () => {
  23. it('onBeforeMount', () => {
  24. const root = nodeOps.createElement('div')
  25. const fn = vi.fn(() => {
  26. // should be called before inner div is rendered
  27. expect(serializeInner(root)).toBe(``)
  28. })
  29. const Comp = {
  30. setup() {
  31. onBeforeMount(fn)
  32. return () => h('div')
  33. }
  34. }
  35. render(h(Comp), root)
  36. expect(fn).toHaveBeenCalledTimes(1)
  37. })
  38. it('onMounted', () => {
  39. const root = nodeOps.createElement('div')
  40. const fn = vi.fn(() => {
  41. // should be called after inner div is rendered
  42. expect(serializeInner(root)).toBe(`<div></div>`)
  43. })
  44. const Comp = {
  45. setup() {
  46. onMounted(fn)
  47. return () => h('div')
  48. }
  49. }
  50. render(h(Comp), root)
  51. expect(fn).toHaveBeenCalledTimes(1)
  52. })
  53. it('onBeforeUpdate', async () => {
  54. const count = ref(0)
  55. const root = nodeOps.createElement('div')
  56. const fn = vi.fn(() => {
  57. // should be called before inner div is updated
  58. expect(serializeInner(root)).toBe(`<div>0</div>`)
  59. })
  60. const Comp = {
  61. setup() {
  62. onBeforeUpdate(fn)
  63. return () => h('div', count.value)
  64. }
  65. }
  66. render(h(Comp), root)
  67. count.value++
  68. await nextTick()
  69. expect(fn).toHaveBeenCalledTimes(1)
  70. expect(serializeInner(root)).toBe(`<div>1</div>`)
  71. })
  72. it('state mutation in onBeforeUpdate', async () => {
  73. const count = ref(0)
  74. const root = nodeOps.createElement('div')
  75. const fn = vi.fn(() => {
  76. // should be called before inner div is updated
  77. expect(serializeInner(root)).toBe(`<div>0</div>`)
  78. count.value++
  79. })
  80. const renderSpy = vi.fn()
  81. const Comp = {
  82. setup() {
  83. onBeforeUpdate(fn)
  84. return () => {
  85. renderSpy()
  86. return h('div', count.value)
  87. }
  88. }
  89. }
  90. render(h(Comp), root)
  91. expect(renderSpy).toHaveBeenCalledTimes(1)
  92. count.value++
  93. await nextTick()
  94. expect(fn).toHaveBeenCalledTimes(1)
  95. expect(renderSpy).toHaveBeenCalledTimes(2)
  96. expect(serializeInner(root)).toBe(`<div>2</div>`)
  97. })
  98. it('onUpdated', async () => {
  99. const count = ref(0)
  100. const root = nodeOps.createElement('div')
  101. const fn = vi.fn(() => {
  102. // should be called after inner div is updated
  103. expect(serializeInner(root)).toBe(`<div>1</div>`)
  104. })
  105. const Comp = {
  106. setup() {
  107. onUpdated(fn)
  108. return () => h('div', count.value)
  109. }
  110. }
  111. render(h(Comp), root)
  112. count.value++
  113. await nextTick()
  114. expect(fn).toHaveBeenCalledTimes(1)
  115. })
  116. it('onBeforeUnmount', async () => {
  117. const toggle = ref(true)
  118. const root = nodeOps.createElement('div')
  119. const fn = vi.fn(() => {
  120. // should be called before inner div is removed
  121. expect(serializeInner(root)).toBe(`<div></div>`)
  122. })
  123. const Comp = {
  124. setup() {
  125. return () => (toggle.value ? h(Child) : null)
  126. }
  127. }
  128. const Child = {
  129. setup() {
  130. onBeforeUnmount(fn)
  131. return () => h('div')
  132. }
  133. }
  134. render(h(Comp), root)
  135. toggle.value = false
  136. await nextTick()
  137. expect(fn).toHaveBeenCalledTimes(1)
  138. })
  139. it('onUnmounted', async () => {
  140. const toggle = ref(true)
  141. const root = nodeOps.createElement('div')
  142. const fn = vi.fn(() => {
  143. // should be called after inner div is removed
  144. expect(serializeInner(root)).toBe(`<!---->`)
  145. })
  146. const Comp = {
  147. setup() {
  148. return () => (toggle.value ? h(Child) : null)
  149. }
  150. }
  151. const Child = {
  152. setup() {
  153. onUnmounted(fn)
  154. return () => h('div')
  155. }
  156. }
  157. render(h(Comp), root)
  158. toggle.value = false
  159. await nextTick()
  160. expect(fn).toHaveBeenCalledTimes(1)
  161. })
  162. it('onBeforeUnmount in onMounted', async () => {
  163. const toggle = ref(true)
  164. const root = nodeOps.createElement('div')
  165. const fn = vi.fn(() => {
  166. // should be called before inner div is removed
  167. expect(serializeInner(root)).toBe(`<div></div>`)
  168. })
  169. const Comp = {
  170. setup() {
  171. return () => (toggle.value ? h(Child) : null)
  172. }
  173. }
  174. const Child = {
  175. setup() {
  176. onMounted(() => {
  177. onBeforeUnmount(fn)
  178. })
  179. return () => h('div')
  180. }
  181. }
  182. render(h(Comp), root)
  183. toggle.value = false
  184. await nextTick()
  185. expect(fn).toHaveBeenCalledTimes(1)
  186. })
  187. it('lifecycle call order', async () => {
  188. const count = ref(0)
  189. const root = nodeOps.createElement('div')
  190. const calls: string[] = []
  191. const Root = {
  192. setup() {
  193. onBeforeMount(() => calls.push('root onBeforeMount'))
  194. onMounted(() => calls.push('root onMounted'))
  195. onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
  196. onUpdated(() => calls.push('root onUpdated'))
  197. onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
  198. onUnmounted(() => calls.push('root onUnmounted'))
  199. return () => h(Mid, { count: count.value })
  200. }
  201. }
  202. const Mid = {
  203. props: ['count'],
  204. setup(props: any) {
  205. onBeforeMount(() => calls.push('mid onBeforeMount'))
  206. onMounted(() => calls.push('mid onMounted'))
  207. onBeforeUpdate(() => calls.push('mid onBeforeUpdate'))
  208. onUpdated(() => calls.push('mid onUpdated'))
  209. onBeforeUnmount(() => calls.push('mid onBeforeUnmount'))
  210. onUnmounted(() => calls.push('mid onUnmounted'))
  211. return () => h(Child, { count: props.count })
  212. }
  213. }
  214. const Child = {
  215. props: ['count'],
  216. setup(props: any) {
  217. onBeforeMount(() => calls.push('child onBeforeMount'))
  218. onMounted(() => calls.push('child onMounted'))
  219. onBeforeUpdate(() => calls.push('child onBeforeUpdate'))
  220. onUpdated(() => calls.push('child onUpdated'))
  221. onBeforeUnmount(() => calls.push('child onBeforeUnmount'))
  222. onUnmounted(() => calls.push('child onUnmounted'))
  223. return () => h('div', props.count)
  224. }
  225. }
  226. // mount
  227. render(h(Root), root)
  228. expect(calls).toEqual([
  229. 'root onBeforeMount',
  230. 'mid onBeforeMount',
  231. 'child onBeforeMount',
  232. 'child onMounted',
  233. 'mid onMounted',
  234. 'root onMounted'
  235. ])
  236. calls.length = 0
  237. // update
  238. count.value++
  239. await nextTick()
  240. expect(calls).toEqual([
  241. 'root onBeforeUpdate',
  242. 'mid onBeforeUpdate',
  243. 'child onBeforeUpdate',
  244. 'child onUpdated',
  245. 'mid onUpdated',
  246. 'root onUpdated'
  247. ])
  248. calls.length = 0
  249. // unmount
  250. render(null, root)
  251. expect(calls).toEqual([
  252. 'root onBeforeUnmount',
  253. 'mid onBeforeUnmount',
  254. 'child onBeforeUnmount',
  255. 'child onUnmounted',
  256. 'mid onUnmounted',
  257. 'root onUnmounted'
  258. ])
  259. })
  260. it('onRenderTracked', () => {
  261. const events: DebuggerEvent[] = []
  262. const onTrack = vi.fn((e: DebuggerEvent) => {
  263. events.push(e)
  264. })
  265. const obj = reactive({ foo: 1, bar: 2 })
  266. const Comp = {
  267. setup() {
  268. onRenderTracked(onTrack)
  269. return () =>
  270. h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  271. }
  272. }
  273. render(h(Comp), nodeOps.createElement('div'))
  274. expect(onTrack).toHaveBeenCalledTimes(3)
  275. expect(events).toMatchObject([
  276. {
  277. target: obj,
  278. type: TrackOpTypes.GET,
  279. key: 'foo'
  280. },
  281. {
  282. target: obj,
  283. type: TrackOpTypes.HAS,
  284. key: 'bar'
  285. },
  286. {
  287. target: obj,
  288. type: TrackOpTypes.ITERATE,
  289. key: ITERATE_KEY
  290. }
  291. ])
  292. })
  293. it('onRenderTriggered', async () => {
  294. const events: DebuggerEvent[] = []
  295. const onTrigger = vi.fn((e: DebuggerEvent) => {
  296. events.push(e)
  297. })
  298. const obj = reactive<{
  299. foo: number
  300. bar?: number
  301. }>({ foo: 1, bar: 2 })
  302. const Comp = {
  303. setup() {
  304. onRenderTriggered(onTrigger)
  305. return () =>
  306. h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  307. }
  308. }
  309. render(h(Comp), nodeOps.createElement('div'))
  310. obj.foo++
  311. await nextTick()
  312. expect(onTrigger).toHaveBeenCalledTimes(1)
  313. expect(events[0]).toMatchObject({
  314. type: TriggerOpTypes.SET,
  315. key: 'foo',
  316. oldValue: 1,
  317. newValue: 2
  318. })
  319. delete obj.bar
  320. await nextTick()
  321. expect(onTrigger).toHaveBeenCalledTimes(2)
  322. expect(events[1]).toMatchObject({
  323. type: TriggerOpTypes.DELETE,
  324. key: 'bar',
  325. oldValue: 2
  326. })
  327. ;(obj as any).baz = 3
  328. await nextTick()
  329. expect(onTrigger).toHaveBeenCalledTimes(3)
  330. expect(events[2]).toMatchObject({
  331. type: TriggerOpTypes.ADD,
  332. key: 'baz',
  333. newValue: 3
  334. })
  335. })
  336. it('runs shared hook fn for each instance', async () => {
  337. const fn = vi.fn()
  338. const toggle = ref(true)
  339. const Comp = {
  340. setup() {
  341. return () => (toggle.value ? [h(Child), h(Child)] : null)
  342. }
  343. }
  344. const Child = {
  345. setup() {
  346. onMounted(fn)
  347. onBeforeUnmount(fn)
  348. return () => h('div')
  349. }
  350. }
  351. render(h(Comp), nodeOps.createElement('div'))
  352. expect(fn).toHaveBeenCalledTimes(2)
  353. toggle.value = false
  354. await nextTick()
  355. expect(fn).toHaveBeenCalledTimes(4)
  356. })
  357. })