apiWatch.spec.ts 11 KB

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