apiLifecycle.spec.ts 11 KB

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