apiWatch.spec.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import {
  2. watch,
  3. reactive,
  4. computed,
  5. nextTick,
  6. ref,
  7. h,
  8. OperationTypes
  9. } from '../src/index'
  10. import { render, nodeOps, serializeInner } from '@vue/runtime-test'
  11. import { ITERATE_KEY, DebuggerEvent } from '@vue/reactivity'
  12. // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
  13. describe('api: watch', () => {
  14. it('basic usage', async () => {
  15. const state = reactive({ count: 0 })
  16. let dummy
  17. watch(() => {
  18. dummy = state.count
  19. })
  20. await nextTick()
  21. expect(dummy).toBe(0)
  22. state.count++
  23. await nextTick()
  24. expect(dummy).toBe(1)
  25. })
  26. it('watching single source: getter', async () => {
  27. const state = reactive({ count: 0 })
  28. let dummy
  29. watch(
  30. () => state.count,
  31. (count, prevCount) => {
  32. dummy = [count, prevCount]
  33. // assert types
  34. count + 1
  35. prevCount + 1
  36. }
  37. )
  38. await nextTick()
  39. expect(dummy).toMatchObject([0, undefined])
  40. state.count++
  41. await nextTick()
  42. expect(dummy).toMatchObject([1, 0])
  43. })
  44. it('watching single source: ref', async () => {
  45. const count = ref(0)
  46. let dummy
  47. watch(count, (count, prevCount) => {
  48. dummy = [count, prevCount]
  49. // assert types
  50. count + 1
  51. prevCount + 1
  52. })
  53. await nextTick()
  54. expect(dummy).toMatchObject([0, undefined])
  55. count.value++
  56. await nextTick()
  57. expect(dummy).toMatchObject([1, 0])
  58. })
  59. it('watching single source: computed ref', async () => {
  60. const count = ref(0)
  61. const plus = computed(() => count.value + 1)
  62. let dummy
  63. watch(plus, (count, prevCount) => {
  64. dummy = [count, prevCount]
  65. // assert types
  66. count + 1
  67. prevCount + 1
  68. })
  69. await nextTick()
  70. expect(dummy).toMatchObject([1, undefined])
  71. count.value++
  72. await nextTick()
  73. expect(dummy).toMatchObject([2, 1])
  74. })
  75. it('watching multiple sources', async () => {
  76. const state = reactive({ count: 1 })
  77. const count = ref(1)
  78. const plus = computed(() => count.value + 1)
  79. let dummy
  80. watch([() => state.count, count, plus], (vals, oldVals) => {
  81. dummy = [vals, oldVals]
  82. // assert types
  83. vals.concat(1)
  84. oldVals.concat(1)
  85. })
  86. await nextTick()
  87. expect(dummy).toMatchObject([[1, 1, 2], []])
  88. state.count++
  89. count.value++
  90. await nextTick()
  91. expect(dummy).toMatchObject([[2, 2, 3], [1, 1, 2]])
  92. })
  93. it('stopping the watcher', async () => {
  94. const state = reactive({ count: 0 })
  95. let dummy
  96. const stop = watch(() => {
  97. dummy = state.count
  98. })
  99. await nextTick()
  100. expect(dummy).toBe(0)
  101. stop()
  102. state.count++
  103. await nextTick()
  104. // should not update
  105. expect(dummy).toBe(0)
  106. })
  107. it('cleanup registration (basic)', async () => {
  108. const state = reactive({ count: 0 })
  109. const cleanup = jest.fn()
  110. let dummy
  111. const stop = watch(onCleanup => {
  112. onCleanup(cleanup)
  113. dummy = state.count
  114. })
  115. await nextTick()
  116. expect(dummy).toBe(0)
  117. state.count++
  118. await nextTick()
  119. expect(cleanup).toHaveBeenCalledTimes(1)
  120. expect(dummy).toBe(1)
  121. stop()
  122. expect(cleanup).toHaveBeenCalledTimes(2)
  123. })
  124. it('cleanup registration (with source)', async () => {
  125. const count = ref(0)
  126. const cleanup = jest.fn()
  127. let dummy
  128. const stop = watch(count, (count, prevCount, onCleanup) => {
  129. onCleanup(cleanup)
  130. dummy = count
  131. })
  132. await nextTick()
  133. expect(dummy).toBe(0)
  134. count.value++
  135. await nextTick()
  136. expect(cleanup).toHaveBeenCalledTimes(1)
  137. expect(dummy).toBe(1)
  138. stop()
  139. expect(cleanup).toHaveBeenCalledTimes(2)
  140. })
  141. it('flush timing: post', async () => {
  142. const count = ref(0)
  143. const assertion = jest.fn(count => {
  144. expect(serializeInner(root)).toBe(`${count}`)
  145. })
  146. const Comp = {
  147. setup() {
  148. watch(() => {
  149. assertion(count.value)
  150. })
  151. return () => count.value
  152. }
  153. }
  154. const root = nodeOps.createElement('div')
  155. render(h(Comp), root)
  156. await nextTick()
  157. expect(assertion).toHaveBeenCalledTimes(1)
  158. count.value++
  159. await nextTick()
  160. expect(assertion).toHaveBeenCalledTimes(2)
  161. })
  162. it('flush timing: pre', async () => {
  163. const count = ref(0)
  164. const count2 = ref(0)
  165. let callCount = 0
  166. const assertion = jest.fn((count, count2Value) => {
  167. callCount++
  168. // on mount, the watcher callback should be called before DOM render
  169. // on update, should be called before the count is updated
  170. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  171. expect(serializeInner(root)).toBe(expectedDOM)
  172. // in a pre-flush callback, all state should have been updated
  173. const expectedState = callCount === 1 ? 0 : 1
  174. expect(count2Value).toBe(expectedState)
  175. })
  176. const Comp = {
  177. setup() {
  178. watch(
  179. () => {
  180. assertion(count.value, count2.value)
  181. },
  182. {
  183. flush: 'pre'
  184. }
  185. )
  186. return () => count.value
  187. }
  188. }
  189. const root = nodeOps.createElement('div')
  190. render(h(Comp), root)
  191. await nextTick()
  192. expect(assertion).toHaveBeenCalledTimes(1)
  193. count.value++
  194. count2.value++
  195. await nextTick()
  196. // two mutations should result in 1 callback execution
  197. expect(assertion).toHaveBeenCalledTimes(2)
  198. })
  199. it('flush timing: sync', async () => {
  200. const count = ref(0)
  201. const count2 = ref(0)
  202. let callCount = 0
  203. const assertion = jest.fn(count => {
  204. callCount++
  205. // on mount, the watcher callback should be called before DOM render
  206. // on update, should be called before the count is updated
  207. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  208. expect(serializeInner(root)).toBe(expectedDOM)
  209. // in a sync callback, state mutation on the next line should not have
  210. // executed yet on the 2nd call, but will be on the 3rd call.
  211. const expectedState = callCount < 3 ? 0 : 1
  212. expect(count2.value).toBe(expectedState)
  213. })
  214. const Comp = {
  215. setup() {
  216. watch(
  217. () => {
  218. assertion(count.value)
  219. },
  220. {
  221. flush: 'sync'
  222. }
  223. )
  224. return () => count.value
  225. }
  226. }
  227. const root = nodeOps.createElement('div')
  228. render(h(Comp), root)
  229. await nextTick()
  230. expect(assertion).toHaveBeenCalledTimes(1)
  231. count.value++
  232. count2.value++
  233. await nextTick()
  234. expect(assertion).toHaveBeenCalledTimes(3)
  235. })
  236. it('deep', async () => {
  237. const state = reactive({
  238. nested: {
  239. count: ref(0)
  240. },
  241. array: [1, 2, 3],
  242. map: new Map([['a', 1], ['b', 2]]),
  243. set: new Set([1, 2, 3])
  244. })
  245. let dummy
  246. watch(
  247. () => state,
  248. state => {
  249. dummy = [
  250. state.nested.count,
  251. state.array[0],
  252. state.map.get('a'),
  253. state.set.has(1)
  254. ]
  255. },
  256. { deep: true }
  257. )
  258. await nextTick()
  259. expect(dummy).toEqual([0, 1, 1, true])
  260. state.nested.count++
  261. await nextTick()
  262. expect(dummy).toEqual([1, 1, 1, true])
  263. // nested array mutation
  264. state.array[0] = 2
  265. await nextTick()
  266. expect(dummy).toEqual([1, 2, 1, true])
  267. // nested map mutation
  268. state.map.set('a', 2)
  269. await nextTick()
  270. expect(dummy).toEqual([1, 2, 2, true])
  271. // nested set mutation
  272. state.set.delete(1)
  273. await nextTick()
  274. expect(dummy).toEqual([1, 2, 2, false])
  275. })
  276. it('lazy', async () => {
  277. const count = ref(0)
  278. const cb = jest.fn()
  279. watch(count, cb, { lazy: true })
  280. await nextTick()
  281. expect(cb).not.toHaveBeenCalled()
  282. count.value++
  283. await nextTick()
  284. expect(cb).toHaveBeenCalled()
  285. })
  286. it('onTrack', async () => {
  287. const events: DebuggerEvent[] = []
  288. let dummy
  289. const onTrack = jest.fn((e: DebuggerEvent) => {
  290. events.push(e)
  291. })
  292. const obj = reactive({ foo: 1, bar: 2 })
  293. watch(
  294. () => {
  295. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  296. },
  297. { onTrack }
  298. )
  299. await nextTick()
  300. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  301. expect(onTrack).toHaveBeenCalledTimes(3)
  302. expect(events).toMatchObject([
  303. {
  304. target: obj,
  305. type: OperationTypes.GET,
  306. key: 'foo'
  307. },
  308. {
  309. target: obj,
  310. type: OperationTypes.HAS,
  311. key: 'bar'
  312. },
  313. {
  314. target: obj,
  315. type: OperationTypes.ITERATE,
  316. key: ITERATE_KEY
  317. }
  318. ])
  319. })
  320. it('onTrigger', async () => {
  321. const events: DebuggerEvent[] = []
  322. let dummy
  323. const onTrigger = jest.fn((e: DebuggerEvent) => {
  324. events.push(e)
  325. })
  326. const obj = reactive({ foo: 1 })
  327. watch(
  328. () => {
  329. dummy = obj.foo
  330. },
  331. { onTrigger }
  332. )
  333. await nextTick()
  334. expect(dummy).toBe(1)
  335. obj.foo++
  336. await nextTick()
  337. expect(dummy).toBe(2)
  338. expect(onTrigger).toHaveBeenCalledTimes(1)
  339. expect(events[0]).toMatchObject({
  340. type: OperationTypes.SET,
  341. key: 'foo',
  342. oldValue: 1,
  343. newValue: 2
  344. })
  345. delete obj.foo
  346. await nextTick()
  347. expect(dummy).toBeUndefined()
  348. expect(onTrigger).toHaveBeenCalledTimes(2)
  349. expect(events[1]).toMatchObject({
  350. type: OperationTypes.DELETE,
  351. key: 'foo',
  352. oldValue: 2
  353. })
  354. })
  355. })