apiLifecycle.spec.ts 9.4 KB

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