apiLifecycle.spec.ts 8.0 KB

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