apiWatch.spec.ts 11 KB

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