apiWatch.spec.ts 11 KB

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