apiWatch.spec.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import {
  2. currentInstance,
  3. effectScope,
  4. nextTick,
  5. onMounted,
  6. onUpdated,
  7. ref,
  8. watch,
  9. watchEffect,
  10. } from '@vue/runtime-dom'
  11. import { createComponent, defineVaporComponent, renderEffect } from '../src'
  12. import { makeRender } from './_utils'
  13. import type { VaporComponentInstance } from '../src/component'
  14. const define = makeRender()
  15. // only need to port test cases related to in-component usage
  16. describe('apiWatch', () => {
  17. // #7030
  18. it.todo(
  19. // need if support
  20. 'should not fire on child component unmount w/ flush: pre',
  21. async () => {
  22. const visible = ref(true)
  23. const cb = vi.fn()
  24. const Parent = defineVaporComponent({
  25. props: ['visible'],
  26. setup() {
  27. // @ts-expect-error
  28. return visible.value ? h(Comp) : null
  29. },
  30. })
  31. const Comp = {
  32. setup() {
  33. watch(visible, cb, { flush: 'pre' })
  34. return []
  35. },
  36. }
  37. define(Parent).render({
  38. visible: () => visible.value,
  39. })
  40. expect(cb).not.toHaveBeenCalled()
  41. visible.value = false
  42. await nextTick()
  43. expect(cb).not.toHaveBeenCalled()
  44. },
  45. )
  46. // #7030
  47. it('flush: pre watcher in child component should not fire before parent update', async () => {
  48. const b = ref(0)
  49. const calls: string[] = []
  50. const Comp = {
  51. setup() {
  52. watch(
  53. () => b.value,
  54. val => {
  55. calls.push('watcher child')
  56. },
  57. { flush: 'pre' },
  58. )
  59. renderEffect(() => {
  60. b.value
  61. calls.push('render child')
  62. })
  63. return []
  64. },
  65. }
  66. const Parent = {
  67. props: ['a'],
  68. setup() {
  69. watch(
  70. () => b.value,
  71. val => {
  72. calls.push('watcher parent')
  73. },
  74. { flush: 'pre' },
  75. )
  76. renderEffect(() => {
  77. b.value
  78. calls.push('render parent')
  79. })
  80. return createComponent(Comp)
  81. },
  82. }
  83. define(Parent).render({
  84. a: () => b.value,
  85. })
  86. expect(calls).toEqual(['render parent', 'render child'])
  87. b.value++
  88. await nextTick()
  89. expect(calls).toEqual([
  90. 'render parent',
  91. 'render child',
  92. 'watcher parent',
  93. 'render parent',
  94. 'watcher child',
  95. 'render child',
  96. ])
  97. })
  98. // #1763
  99. it('flush: pre watcher watching props should fire before child update', async () => {
  100. const a = ref(0)
  101. const b = ref(0)
  102. const c = ref(0)
  103. const calls: string[] = []
  104. const Comp = {
  105. props: ['a', 'b'],
  106. setup(props: any) {
  107. watch(
  108. () => props.a + props.b,
  109. () => {
  110. calls.push('watcher 1')
  111. c.value++
  112. },
  113. { flush: 'pre' },
  114. )
  115. // #1777 chained pre-watcher
  116. watch(
  117. c,
  118. () => {
  119. calls.push('watcher 2')
  120. },
  121. { flush: 'pre' },
  122. )
  123. renderEffect(() => {
  124. c.value
  125. calls.push('render')
  126. })
  127. return []
  128. },
  129. }
  130. define(Comp).render({
  131. a: () => a.value,
  132. b: () => b.value,
  133. })
  134. expect(calls).toEqual(['render'])
  135. // both props are updated
  136. // should trigger pre-flush watcher first and only once
  137. // then trigger child render
  138. a.value++
  139. b.value++
  140. await nextTick()
  141. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  142. })
  143. // #5721
  144. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  145. const count = ref(0)
  146. const calls: string[] = []
  147. const App = {
  148. setup() {
  149. watch(
  150. count,
  151. () => {
  152. calls.push('watch ' + count.value)
  153. },
  154. { flush: 'pre' },
  155. )
  156. onMounted(() => {
  157. calls.push('mounted')
  158. })
  159. // mutate multiple times
  160. count.value++
  161. count.value++
  162. count.value++
  163. return []
  164. },
  165. }
  166. define(App).render()
  167. expect(calls).toMatchObject(['watch 3', 'mounted'])
  168. })
  169. // #1852
  170. it.todo(
  171. // need if + templateRef
  172. 'flush: post watcher should fire after template refs updated',
  173. async () => {
  174. const toggle = ref(false)
  175. let dom: Element | null = null
  176. const App = {
  177. setup() {
  178. const domRef = ref<Element | null>(null)
  179. watch(
  180. toggle,
  181. () => {
  182. dom = domRef.value
  183. },
  184. { flush: 'post' },
  185. )
  186. return () => {
  187. // @ts-expect-error
  188. return toggle.value ? h('p', { ref: domRef }) : null
  189. }
  190. },
  191. }
  192. // @ts-expect-error
  193. render(h(App), nodeOps.createElement('div'))
  194. expect(dom).toBe(null)
  195. toggle.value = true
  196. await nextTick()
  197. expect(dom!.tagName).toBe('P')
  198. },
  199. )
  200. test('should not leak `this.proxy` to setup()', () => {
  201. const source = vi.fn()
  202. const Comp = defineVaporComponent({
  203. setup() {
  204. watch(source, () => {})
  205. return []
  206. },
  207. })
  208. define(Comp).render()
  209. // should not have any arguments
  210. expect(source.mock.calls[0]).toMatchObject([])
  211. })
  212. // #2728
  213. test('pre watcher callbacks should not track dependencies', async () => {
  214. const a = ref(0)
  215. const b = ref(0)
  216. const updated = vi.fn()
  217. const Comp = defineVaporComponent({
  218. props: ['a'],
  219. setup(props) {
  220. onUpdated(updated)
  221. watch(
  222. () => props.a,
  223. () => {
  224. b.value
  225. },
  226. )
  227. renderEffect(() => {
  228. props.a
  229. })
  230. return []
  231. },
  232. })
  233. define(Comp).render({
  234. a: () => a.value,
  235. })
  236. a.value++
  237. await nextTick()
  238. expect(updated).toHaveBeenCalledTimes(1)
  239. b.value++
  240. await nextTick()
  241. // should not track b as dependency of Child
  242. expect(updated).toHaveBeenCalledTimes(1)
  243. })
  244. // #4158
  245. test('watch should not register in owner component if created inside detached scope', () => {
  246. let instance: VaporComponentInstance
  247. const Comp = {
  248. setup() {
  249. instance = currentInstance as VaporComponentInstance
  250. effectScope(true).run(() => {
  251. watch(
  252. () => 1,
  253. () => {},
  254. )
  255. })
  256. return []
  257. },
  258. }
  259. define(Comp).render()
  260. // should not record watcher in detached scope
  261. expect(instance!.scope.effects.length).toBe(0)
  262. })
  263. test('watchEffect should keep running if created in a detached scope', async () => {
  264. const trigger = ref(0)
  265. let countWE = 0
  266. let countW = 0
  267. const Comp = {
  268. setup() {
  269. effectScope(true).run(() => {
  270. watchEffect(() => {
  271. trigger.value
  272. countWE++
  273. })
  274. watch(trigger, () => countW++)
  275. })
  276. return []
  277. },
  278. }
  279. const { app } = define(Comp).render()
  280. // only watchEffect as ran so far
  281. expect(countWE).toBe(1)
  282. expect(countW).toBe(0)
  283. trigger.value++
  284. await nextTick()
  285. // both watchers run while component is mounted
  286. expect(countWE).toBe(2)
  287. expect(countW).toBe(1)
  288. app.unmount()
  289. await nextTick()
  290. trigger.value++
  291. await nextTick()
  292. // both watchers run again event though component has been unmounted
  293. expect(countWE).toBe(3)
  294. expect(countW).toBe(2)
  295. })
  296. })