watch.spec.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {
  2. EffectScope,
  3. type Ref,
  4. WatchErrorCodes,
  5. type WatchOptions,
  6. computed,
  7. onWatcherCleanup,
  8. ref,
  9. watch,
  10. } from '../src'
  11. describe('watch', () => {
  12. test('effect', () => {
  13. let dummy: any
  14. const source = ref(0)
  15. watch(() => {
  16. dummy = source.value
  17. })
  18. expect(dummy).toBe(0)
  19. source.value++
  20. expect(dummy).toBe(1)
  21. })
  22. test('with callback', () => {
  23. let dummy: any
  24. const source = ref(0)
  25. watch(source, () => {
  26. dummy = source.value
  27. })
  28. expect(dummy).toBe(undefined)
  29. source.value++
  30. expect(dummy).toBe(1)
  31. })
  32. test('call option with error handling', () => {
  33. const onError = vi.fn()
  34. const call: WatchOptions['call'] = function call(fn, type, args) {
  35. if (Array.isArray(fn)) {
  36. fn.forEach(f => call(f, type, args))
  37. return
  38. }
  39. try {
  40. fn.apply(null, args)
  41. } catch (e) {
  42. onError(e, type)
  43. }
  44. }
  45. watch(
  46. () => {
  47. throw 'oops in effect'
  48. },
  49. null,
  50. { call },
  51. )
  52. const source = ref(0)
  53. const effect = watch(
  54. source,
  55. () => {
  56. onWatcherCleanup(() => {
  57. throw 'oops in cleanup'
  58. })
  59. throw 'oops in watch'
  60. },
  61. { call },
  62. )
  63. expect(onError.mock.calls.length).toBe(1)
  64. expect(onError.mock.calls[0]).toMatchObject([
  65. 'oops in effect',
  66. WatchErrorCodes.WATCH_CALLBACK,
  67. ])
  68. source.value++
  69. expect(onError.mock.calls.length).toBe(2)
  70. expect(onError.mock.calls[1]).toMatchObject([
  71. 'oops in watch',
  72. WatchErrorCodes.WATCH_CALLBACK,
  73. ])
  74. effect!.stop()
  75. source.value++
  76. expect(onError.mock.calls.length).toBe(3)
  77. expect(onError.mock.calls[2]).toMatchObject([
  78. 'oops in cleanup',
  79. WatchErrorCodes.WATCH_CLEANUP,
  80. ])
  81. })
  82. test('watch with onWatcherCleanup', async () => {
  83. let dummy = 0
  84. let source: Ref<number>
  85. const scope = new EffectScope()
  86. scope.run(() => {
  87. source = ref(0)
  88. watch(onCleanup => {
  89. source.value
  90. onCleanup(() => (dummy += 2))
  91. onWatcherCleanup(() => (dummy += 3))
  92. onWatcherCleanup(() => (dummy += 5))
  93. })
  94. })
  95. expect(dummy).toBe(0)
  96. scope.run(() => {
  97. source.value++
  98. })
  99. expect(dummy).toBe(10)
  100. scope.run(() => {
  101. source.value++
  102. })
  103. expect(dummy).toBe(20)
  104. scope.stop()
  105. expect(dummy).toBe(30)
  106. })
  107. test('once option should be ignored by simple watch', async () => {
  108. let dummy: any
  109. const source = ref(0)
  110. watch(
  111. () => {
  112. dummy = source.value
  113. },
  114. null,
  115. { once: true },
  116. )
  117. expect(dummy).toBe(0)
  118. source.value++
  119. expect(dummy).toBe(1)
  120. })
  121. // #12033
  122. test('recursive sync watcher on computed', () => {
  123. const r = ref(0)
  124. const c = computed(() => r.value)
  125. watch(c, v => {
  126. if (v > 1) {
  127. r.value--
  128. }
  129. })
  130. expect(r.value).toBe(0)
  131. expect(c.value).toBe(0)
  132. r.value = 10
  133. expect(r.value).toBe(1)
  134. expect(c.value).toBe(1)
  135. })
  136. // edge case where a nested endBatch() causes an effect to be batched in a
  137. // nested batch loop with its .next mutated, causing the outer loop to end
  138. // early
  139. test('nested batch edge case', () => {
  140. // useClamp from VueUse
  141. const clamp = (n: number, min: number, max: number) =>
  142. Math.min(max, Math.max(min, n))
  143. function useClamp(src: Ref<number>, min: number, max: number) {
  144. return computed({
  145. get() {
  146. return (src.value = clamp(src.value, min, max))
  147. },
  148. set(val) {
  149. src.value = clamp(val, min, max)
  150. },
  151. })
  152. }
  153. const src = ref(1)
  154. const clamped = useClamp(src, 1, 5)
  155. watch(src, val => (clamped.value = val))
  156. const spy = vi.fn()
  157. watch(clamped, spy)
  158. src.value = 2
  159. expect(spy).toHaveBeenCalledTimes(1)
  160. src.value = 10
  161. expect(spy).toHaveBeenCalledTimes(2)
  162. })
  163. test('should ensure correct execution order in batch processing', () => {
  164. const dummy: number[] = []
  165. const n1 = ref(0)
  166. const n2 = ref(0)
  167. const sum = computed(() => n1.value + n2.value)
  168. watch(n1, () => {
  169. dummy.push(1)
  170. n2.value++
  171. })
  172. watch(sum, () => dummy.push(2))
  173. watch(n1, () => dummy.push(3))
  174. n1.value++
  175. expect(dummy).toEqual([1, 2, 3])
  176. })
  177. test('watch with immediate reset and sync flush', () => {
  178. const value = ref(false)
  179. watch(value, () => {
  180. value.value = false
  181. })
  182. value.value = true
  183. value.value = true
  184. expect(value.value).toBe(false)
  185. })
  186. })