watch.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import {
  2. EffectScope,
  3. type Ref,
  4. WatchErrorCodes,
  5. type WatchOptions,
  6. type WatchScheduler,
  7. onWatcherCleanup,
  8. ref,
  9. watch,
  10. } from '../src'
  11. const queue: (() => void)[] = []
  12. // a simple scheduler for testing purposes
  13. let isFlushPending = false
  14. const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
  15. const nextTick = (fn?: () => any) =>
  16. fn ? resolvedPromise.then(fn) : resolvedPromise
  17. const scheduler: WatchScheduler = (job, isFirstRun) => {
  18. if (isFirstRun) {
  19. job()
  20. } else {
  21. queue.push(job)
  22. flushJobs()
  23. }
  24. }
  25. const flushJobs = () => {
  26. if (isFlushPending) return
  27. isFlushPending = true
  28. resolvedPromise.then(() => {
  29. queue.forEach(job => job())
  30. queue.length = 0
  31. isFlushPending = false
  32. })
  33. }
  34. describe('watch', () => {
  35. test('effect', () => {
  36. let dummy: any
  37. const source = ref(0)
  38. watch(() => {
  39. dummy = source.value
  40. })
  41. expect(dummy).toBe(0)
  42. source.value++
  43. expect(dummy).toBe(1)
  44. })
  45. test('with callback', () => {
  46. let dummy: any
  47. const source = ref(0)
  48. watch(source, () => {
  49. dummy = source.value
  50. })
  51. expect(dummy).toBe(undefined)
  52. source.value++
  53. expect(dummy).toBe(1)
  54. })
  55. test('call option with error handling', () => {
  56. const onError = vi.fn()
  57. const call: WatchOptions['call'] = function call(fn, type, args) {
  58. if (Array.isArray(fn)) {
  59. fn.forEach(f => call(f, type, args))
  60. return
  61. }
  62. try {
  63. fn.apply(null, args)
  64. } catch (e) {
  65. onError(e, type)
  66. }
  67. }
  68. watch(
  69. () => {
  70. throw 'oops in effect'
  71. },
  72. null,
  73. { call },
  74. )
  75. const source = ref(0)
  76. const effect = watch(
  77. source,
  78. () => {
  79. onWatcherCleanup(() => {
  80. throw 'oops in cleanup'
  81. })
  82. throw 'oops in watch'
  83. },
  84. { call },
  85. )
  86. expect(onError.mock.calls.length).toBe(1)
  87. expect(onError.mock.calls[0]).toMatchObject([
  88. 'oops in effect',
  89. WatchErrorCodes.WATCH_CALLBACK,
  90. ])
  91. source.value++
  92. expect(onError.mock.calls.length).toBe(2)
  93. expect(onError.mock.calls[1]).toMatchObject([
  94. 'oops in watch',
  95. WatchErrorCodes.WATCH_CALLBACK,
  96. ])
  97. effect!.stop()
  98. source.value++
  99. expect(onError.mock.calls.length).toBe(3)
  100. expect(onError.mock.calls[2]).toMatchObject([
  101. 'oops in cleanup',
  102. WatchErrorCodes.WATCH_CLEANUP,
  103. ])
  104. })
  105. test('watch with onWatcherCleanup', async () => {
  106. let dummy = 0
  107. let source: Ref<number>
  108. const scope = new EffectScope()
  109. scope.run(() => {
  110. source = ref(0)
  111. watch(onCleanup => {
  112. source.value
  113. onCleanup(() => (dummy += 2))
  114. onWatcherCleanup(() => (dummy += 3))
  115. onWatcherCleanup(() => (dummy += 5))
  116. })
  117. })
  118. expect(dummy).toBe(0)
  119. scope.run(() => {
  120. source.value++
  121. })
  122. expect(dummy).toBe(10)
  123. scope.run(() => {
  124. source.value++
  125. })
  126. expect(dummy).toBe(20)
  127. scope.stop()
  128. expect(dummy).toBe(30)
  129. })
  130. test('nested calls to baseWatch and onWatcherCleanup', async () => {
  131. let calls: string[] = []
  132. let source: Ref<number>
  133. let copyist: Ref<number>
  134. const scope = new EffectScope()
  135. scope.run(() => {
  136. source = ref(0)
  137. copyist = ref(0)
  138. // sync by default
  139. watch(
  140. () => {
  141. const current = (copyist.value = source.value)
  142. onWatcherCleanup(() => calls.push(`sync ${current}`))
  143. },
  144. null,
  145. {},
  146. )
  147. // with scheduler
  148. watch(
  149. () => {
  150. const current = copyist.value
  151. onWatcherCleanup(() => calls.push(`post ${current}`))
  152. },
  153. null,
  154. { scheduler },
  155. )
  156. })
  157. await nextTick()
  158. expect(calls).toEqual([])
  159. scope.run(() => source.value++)
  160. expect(calls).toEqual(['sync 0'])
  161. await nextTick()
  162. expect(calls).toEqual(['sync 0', 'post 0'])
  163. calls.length = 0
  164. scope.run(() => source.value++)
  165. expect(calls).toEqual(['sync 1'])
  166. await nextTick()
  167. expect(calls).toEqual(['sync 1', 'post 1'])
  168. calls.length = 0
  169. scope.stop()
  170. expect(calls).toEqual(['sync 2', 'post 2'])
  171. })
  172. })