apiWatch.spec.ts 7.5 KB

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