apiLifecycle.spec.ts 9.5 KB

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