apiWatch.spec.ts 7.3 KB

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