apiWatch.spec.ts 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709
  1. import {
  2. type ComponentInternalInstance,
  3. type ComponentPublicInstance,
  4. computed,
  5. defineComponent,
  6. getCurrentInstance,
  7. nextTick,
  8. onErrorCaptured,
  9. onWatcherCleanup,
  10. reactive,
  11. ref,
  12. watch,
  13. watchEffect,
  14. } from '../src/index'
  15. import {
  16. type TestElement,
  17. createApp,
  18. h,
  19. nodeOps,
  20. onMounted,
  21. render,
  22. serializeInner,
  23. watchPostEffect,
  24. watchSyncEffect,
  25. } from '@vue/runtime-test'
  26. import {
  27. type DebuggerEvent,
  28. EffectFlags,
  29. ITERATE_KEY,
  30. type Ref,
  31. type ShallowRef,
  32. TrackOpTypes,
  33. TriggerOpTypes,
  34. effectScope,
  35. shallowReactive,
  36. shallowRef,
  37. toRef,
  38. triggerRef,
  39. } from '@vue/reactivity'
  40. describe('api: watch', () => {
  41. it('effect', async () => {
  42. const state = reactive({ count: 0 })
  43. let dummy
  44. watchEffect(() => {
  45. dummy = state.count
  46. })
  47. expect(dummy).toBe(0)
  48. state.count++
  49. await nextTick()
  50. expect(dummy).toBe(1)
  51. })
  52. it('watching single source: getter', async () => {
  53. const state = reactive({ count: 0 })
  54. let dummy
  55. watch(
  56. () => state.count,
  57. (count, prevCount) => {
  58. dummy = [count, prevCount]
  59. // assert types
  60. count + 1
  61. if (prevCount) {
  62. prevCount + 1
  63. }
  64. },
  65. )
  66. state.count++
  67. await nextTick()
  68. expect(dummy).toMatchObject([1, 0])
  69. })
  70. it('watching single source: ref', async () => {
  71. const count = ref(0)
  72. let dummy
  73. watch(count, (count, prevCount) => {
  74. dummy = [count, prevCount]
  75. // assert types
  76. count + 1
  77. if (prevCount) {
  78. prevCount + 1
  79. }
  80. })
  81. count.value++
  82. await nextTick()
  83. expect(dummy).toMatchObject([1, 0])
  84. })
  85. it('watching single source: array', async () => {
  86. const array = reactive([] as number[])
  87. const spy = vi.fn()
  88. watch(array, spy)
  89. array.push(1)
  90. await nextTick()
  91. expect(spy).toBeCalledTimes(1)
  92. expect(spy).toBeCalledWith([1], [1], expect.anything())
  93. })
  94. it('should not call functions inside a reactive source array', () => {
  95. const spy1 = vi.fn()
  96. const array = reactive([spy1])
  97. const spy2 = vi.fn()
  98. watch(array, spy2, { immediate: true })
  99. expect(spy1).toBeCalledTimes(0)
  100. expect(spy2).toBeCalledWith([spy1], undefined, expect.anything())
  101. })
  102. it('should not unwrap refs in a reactive source array', async () => {
  103. const val = ref({ foo: 1 })
  104. const array = reactive([val])
  105. const spy = vi.fn()
  106. watch(array, spy, { immediate: true })
  107. expect(spy).toBeCalledTimes(1)
  108. expect(spy).toBeCalledWith([val], undefined, expect.anything())
  109. // deep by default
  110. val.value.foo++
  111. await nextTick()
  112. expect(spy).toBeCalledTimes(2)
  113. expect(spy).toBeCalledWith([val], [val], expect.anything())
  114. })
  115. it('should not fire if watched getter result did not change', async () => {
  116. const spy = vi.fn()
  117. const n = ref(0)
  118. watch(() => n.value % 2, spy)
  119. n.value++
  120. await nextTick()
  121. expect(spy).toBeCalledTimes(1)
  122. n.value += 2
  123. await nextTick()
  124. // should not be called again because getter result did not change
  125. expect(spy).toBeCalledTimes(1)
  126. })
  127. it('watching single source: computed ref', async () => {
  128. const count = ref(0)
  129. const plus = computed(() => count.value + 1)
  130. let dummy
  131. watch(plus, (count, prevCount) => {
  132. dummy = [count, prevCount]
  133. // assert types
  134. count + 1
  135. if (prevCount) {
  136. prevCount + 1
  137. }
  138. })
  139. count.value++
  140. await nextTick()
  141. expect(dummy).toMatchObject([2, 1])
  142. })
  143. it('watching primitive with deep: true', async () => {
  144. const count = ref(0)
  145. let dummy
  146. watch(
  147. count,
  148. (c, prevCount) => {
  149. dummy = [c, prevCount]
  150. },
  151. {
  152. deep: true,
  153. },
  154. )
  155. count.value++
  156. await nextTick()
  157. expect(dummy).toMatchObject([1, 0])
  158. })
  159. it('directly watching reactive object (with automatic deep: true)', async () => {
  160. const src = reactive({
  161. count: 0,
  162. })
  163. let dummy
  164. watch(src, ({ count }) => {
  165. dummy = count
  166. })
  167. src.count++
  168. await nextTick()
  169. expect(dummy).toBe(1)
  170. })
  171. it('directly watching reactive object with explicit deep: false', async () => {
  172. const src = reactive({
  173. state: {
  174. count: 0,
  175. },
  176. })
  177. let dummy
  178. watch(
  179. src,
  180. ({ state }) => {
  181. dummy = state?.count
  182. },
  183. {
  184. deep: false,
  185. },
  186. )
  187. // nested should not trigger
  188. src.state.count++
  189. await nextTick()
  190. expect(dummy).toBe(undefined)
  191. // root level should trigger
  192. src.state = { count: 1 }
  193. await nextTick()
  194. expect(dummy).toBe(1)
  195. })
  196. it('directly watching reactive array with explicit deep: false', async () => {
  197. const val = ref(1)
  198. const array: any[] = reactive([val])
  199. const spy = vi.fn()
  200. watch(array, spy, { immediate: true, deep: false })
  201. expect(spy).toBeCalledTimes(1)
  202. expect(spy).toBeCalledWith([val], undefined, expect.anything())
  203. val.value++
  204. await nextTick()
  205. expect(spy).toBeCalledTimes(1)
  206. array[1] = 2
  207. await nextTick()
  208. expect(spy).toBeCalledTimes(2)
  209. expect(spy).toBeCalledWith([val, 2], [val, 2], expect.anything())
  210. })
  211. // #9916
  212. it('watching shallow reactive array with deep: false', async () => {
  213. class foo {
  214. prop1: ShallowRef<string> = shallowRef('')
  215. prop2: string = ''
  216. }
  217. const obj1 = new foo()
  218. const obj2 = new foo()
  219. const collection = shallowReactive([obj1, obj2])
  220. const cb = vi.fn()
  221. watch(collection, cb, { deep: false })
  222. collection[0].prop1.value = 'foo'
  223. await nextTick()
  224. // should not trigger
  225. expect(cb).toBeCalledTimes(0)
  226. collection.push(new foo())
  227. await nextTick()
  228. // should trigger on array self mutation
  229. expect(cb).toBeCalledTimes(1)
  230. })
  231. it('should still respect deep: true on shallowReactive source', async () => {
  232. const obj = reactive({ a: 1 })
  233. const arr = shallowReactive([obj])
  234. let dummy
  235. watch(
  236. arr,
  237. () => {
  238. dummy = arr[0].a
  239. },
  240. { deep: true },
  241. )
  242. obj.a++
  243. await nextTick()
  244. expect(dummy).toBe(2)
  245. })
  246. it('watching multiple sources', async () => {
  247. const state = reactive({ count: 1 })
  248. const count = ref(1)
  249. const plus = computed(() => count.value + 1)
  250. let dummy
  251. watch([() => state.count, count, plus], (vals, oldVals) => {
  252. dummy = [vals, oldVals]
  253. // assert types
  254. vals.concat(1)
  255. oldVals.concat(1)
  256. })
  257. state.count++
  258. count.value++
  259. await nextTick()
  260. expect(dummy).toMatchObject([
  261. [2, 2, 3],
  262. [1, 1, 2],
  263. ])
  264. })
  265. it('watching multiple sources: undefined initial values and immediate: true', async () => {
  266. const a = ref()
  267. const b = ref()
  268. let called = false
  269. watch(
  270. [a, b],
  271. ([newA, newB], [oldA, oldB]) => {
  272. called = true
  273. expect([newA, newB]).toMatchObject([undefined, undefined])
  274. expect([oldA, oldB]).toMatchObject([undefined, undefined])
  275. },
  276. { immediate: true },
  277. )
  278. await nextTick()
  279. expect(called).toBe(true)
  280. })
  281. it('watching multiple sources: readonly array', async () => {
  282. const state = reactive({ count: 1 })
  283. const status = ref(false)
  284. let dummy
  285. watch([() => state.count, status] as const, (vals, oldVals) => {
  286. dummy = [vals, oldVals]
  287. const [count] = vals
  288. const [, oldStatus] = oldVals
  289. // assert types
  290. count + 1
  291. oldStatus === true
  292. })
  293. state.count++
  294. status.value = true
  295. await nextTick()
  296. expect(dummy).toMatchObject([
  297. [2, true],
  298. [1, false],
  299. ])
  300. })
  301. it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
  302. const src = reactive({ count: 0 })
  303. let dummy
  304. watch([src], ([state]) => {
  305. dummy = state
  306. // assert types
  307. state.count === 1
  308. })
  309. src.count++
  310. await nextTick()
  311. expect(dummy).toMatchObject({ count: 1 })
  312. })
  313. it('warn invalid watch source', () => {
  314. // @ts-expect-error
  315. watch(1, () => {})
  316. expect(`Invalid watch source`).toHaveBeenWarned()
  317. })
  318. it('warn invalid watch source: multiple sources', () => {
  319. watch([1], () => {})
  320. expect(`Invalid watch source`).toHaveBeenWarned()
  321. })
  322. it('stopping the watcher (effect)', async () => {
  323. const state = reactive({ count: 0 })
  324. let dummy
  325. const stop = watchEffect(() => {
  326. dummy = state.count
  327. })
  328. expect(dummy).toBe(0)
  329. stop()
  330. state.count++
  331. await nextTick()
  332. // should not update
  333. expect(dummy).toBe(0)
  334. })
  335. it('stopping the watcher (with source)', async () => {
  336. const state = reactive({ count: 0 })
  337. let dummy
  338. const stop = watch(
  339. () => state.count,
  340. count => {
  341. dummy = count
  342. },
  343. )
  344. state.count++
  345. await nextTick()
  346. expect(dummy).toBe(1)
  347. stop()
  348. state.count++
  349. await nextTick()
  350. // should not update
  351. expect(dummy).toBe(1)
  352. })
  353. it('cleanup registration (effect)', async () => {
  354. const state = reactive({ count: 0 })
  355. const cleanup = vi.fn()
  356. let dummy
  357. const stop = watchEffect(onCleanup => {
  358. onCleanup(cleanup)
  359. dummy = state.count
  360. })
  361. expect(dummy).toBe(0)
  362. state.count++
  363. await nextTick()
  364. expect(cleanup).toHaveBeenCalledTimes(1)
  365. expect(dummy).toBe(1)
  366. stop()
  367. expect(cleanup).toHaveBeenCalledTimes(2)
  368. })
  369. it('cleanup registration (with source)', async () => {
  370. const count = ref(0)
  371. const cleanup = vi.fn()
  372. let dummy
  373. const stop = watch(count, (count, prevCount, onCleanup) => {
  374. onCleanup(cleanup)
  375. dummy = count
  376. })
  377. count.value++
  378. await nextTick()
  379. expect(cleanup).toHaveBeenCalledTimes(0)
  380. expect(dummy).toBe(1)
  381. count.value++
  382. await nextTick()
  383. expect(cleanup).toHaveBeenCalledTimes(1)
  384. expect(dummy).toBe(2)
  385. stop()
  386. expect(cleanup).toHaveBeenCalledTimes(2)
  387. })
  388. it('onWatcherCleanup', async () => {
  389. const count = ref(0)
  390. const cleanupEffect = vi.fn()
  391. const cleanupWatch = vi.fn()
  392. const stopEffect = watchEffect(() => {
  393. onWatcherCleanup(cleanupEffect)
  394. count.value
  395. })
  396. const stopWatch = watch(count, () => {
  397. onWatcherCleanup(cleanupWatch)
  398. })
  399. count.value++
  400. await nextTick()
  401. expect(cleanupEffect).toHaveBeenCalledTimes(1)
  402. expect(cleanupWatch).toHaveBeenCalledTimes(0)
  403. count.value++
  404. await nextTick()
  405. expect(cleanupEffect).toHaveBeenCalledTimes(2)
  406. expect(cleanupWatch).toHaveBeenCalledTimes(1)
  407. stopEffect()
  408. expect(cleanupEffect).toHaveBeenCalledTimes(3)
  409. stopWatch()
  410. expect(cleanupWatch).toHaveBeenCalledTimes(2)
  411. })
  412. it('flush timing: pre (default)', async () => {
  413. const count = ref(0)
  414. const count2 = ref(0)
  415. let callCount = 0
  416. let result1
  417. let result2
  418. const assertion = vi.fn((count, count2Value) => {
  419. callCount++
  420. // on mount, the watcher callback should be called before DOM render
  421. // on update, should be called before the count is updated
  422. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  423. result1 = serializeInner(root) === expectedDOM
  424. // in a pre-flush callback, all state should have been updated
  425. const expectedState = callCount - 1
  426. result2 = count === expectedState && count2Value === expectedState
  427. })
  428. const Comp = {
  429. setup() {
  430. watchEffect(() => {
  431. assertion(count.value, count2.value)
  432. })
  433. return () => count.value
  434. },
  435. }
  436. const root = nodeOps.createElement('div')
  437. render(h(Comp), root)
  438. expect(assertion).toHaveBeenCalledTimes(1)
  439. expect(result1).toBe(true)
  440. expect(result2).toBe(true)
  441. count.value++
  442. count2.value++
  443. await nextTick()
  444. // two mutations should result in 1 callback execution
  445. expect(assertion).toHaveBeenCalledTimes(2)
  446. expect(result1).toBe(true)
  447. expect(result2).toBe(true)
  448. })
  449. it('flush timing: post', async () => {
  450. const count = ref(0)
  451. let result
  452. const assertion = vi.fn(count => {
  453. result = serializeInner(root) === `${count}`
  454. })
  455. const Comp = {
  456. setup() {
  457. watchEffect(
  458. () => {
  459. assertion(count.value)
  460. },
  461. { flush: 'post' },
  462. )
  463. return () => count.value
  464. },
  465. }
  466. const root = nodeOps.createElement('div')
  467. render(h(Comp), root)
  468. expect(assertion).toHaveBeenCalledTimes(1)
  469. expect(result).toBe(true)
  470. count.value++
  471. await nextTick()
  472. expect(assertion).toHaveBeenCalledTimes(2)
  473. expect(result).toBe(true)
  474. })
  475. it('watchPostEffect', async () => {
  476. const count = ref(0)
  477. let result
  478. const assertion = vi.fn(count => {
  479. result = serializeInner(root) === `${count}`
  480. })
  481. const Comp = {
  482. setup() {
  483. watchPostEffect(() => {
  484. assertion(count.value)
  485. })
  486. return () => count.value
  487. },
  488. }
  489. const root = nodeOps.createElement('div')
  490. render(h(Comp), root)
  491. expect(assertion).toHaveBeenCalledTimes(1)
  492. expect(result).toBe(true)
  493. count.value++
  494. await nextTick()
  495. expect(assertion).toHaveBeenCalledTimes(2)
  496. expect(result).toBe(true)
  497. })
  498. it('flush timing: sync', async () => {
  499. const count = ref(0)
  500. const count2 = ref(0)
  501. let callCount = 0
  502. let result1
  503. let result2
  504. const assertion = vi.fn(count => {
  505. callCount++
  506. // on mount, the watcher callback should be called before DOM render
  507. // on update, should be called before the count is updated
  508. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  509. result1 = serializeInner(root) === expectedDOM
  510. // in a sync callback, state mutation on the next line should not have
  511. // executed yet on the 2nd call, but will be on the 3rd call.
  512. const expectedState = callCount < 3 ? 0 : 1
  513. result2 = count2.value === expectedState
  514. })
  515. const Comp = {
  516. setup() {
  517. watchEffect(
  518. () => {
  519. assertion(count.value)
  520. },
  521. {
  522. flush: 'sync',
  523. },
  524. )
  525. return () => count.value
  526. },
  527. }
  528. const root = nodeOps.createElement('div')
  529. render(h(Comp), root)
  530. expect(assertion).toHaveBeenCalledTimes(1)
  531. expect(result1).toBe(true)
  532. expect(result2).toBe(true)
  533. count.value++
  534. count2.value++
  535. await nextTick()
  536. expect(assertion).toHaveBeenCalledTimes(3)
  537. expect(result1).toBe(true)
  538. expect(result2).toBe(true)
  539. })
  540. it('watchSyncEffect', async () => {
  541. const count = ref(0)
  542. const count2 = ref(0)
  543. let callCount = 0
  544. let result1
  545. let result2
  546. const assertion = vi.fn(count => {
  547. callCount++
  548. // on mount, the watcher callback should be called before DOM render
  549. // on update, should be called before the count is updated
  550. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  551. result1 = serializeInner(root) === expectedDOM
  552. // in a sync callback, state mutation on the next line should not have
  553. // executed yet on the 2nd call, but will be on the 3rd call.
  554. const expectedState = callCount < 3 ? 0 : 1
  555. result2 = count2.value === expectedState
  556. })
  557. const Comp = {
  558. setup() {
  559. watchSyncEffect(() => {
  560. assertion(count.value)
  561. })
  562. return () => count.value
  563. },
  564. }
  565. const root = nodeOps.createElement('div')
  566. render(h(Comp), root)
  567. expect(assertion).toHaveBeenCalledTimes(1)
  568. expect(result1).toBe(true)
  569. expect(result2).toBe(true)
  570. count.value++
  571. count2.value++
  572. await nextTick()
  573. expect(assertion).toHaveBeenCalledTimes(3)
  574. expect(result1).toBe(true)
  575. expect(result2).toBe(true)
  576. })
  577. it('should not fire on component unmount w/ flush: post', async () => {
  578. const toggle = ref(true)
  579. const cb = vi.fn()
  580. const Comp = {
  581. setup() {
  582. watch(toggle, cb, { flush: 'post' })
  583. },
  584. render() {},
  585. }
  586. const App = {
  587. render() {
  588. return toggle.value ? h(Comp) : null
  589. },
  590. }
  591. render(h(App), nodeOps.createElement('div'))
  592. expect(cb).not.toHaveBeenCalled()
  593. toggle.value = false
  594. await nextTick()
  595. expect(cb).not.toHaveBeenCalled()
  596. })
  597. // #2291
  598. it('should not fire on component unmount w/ flush: pre', async () => {
  599. const toggle = ref(true)
  600. const cb = vi.fn()
  601. const Comp = {
  602. setup() {
  603. watch(toggle, cb, { flush: 'pre' })
  604. },
  605. render() {},
  606. }
  607. const App = {
  608. render() {
  609. return toggle.value ? h(Comp) : null
  610. },
  611. }
  612. render(h(App), nodeOps.createElement('div'))
  613. expect(cb).not.toHaveBeenCalled()
  614. toggle.value = false
  615. await nextTick()
  616. expect(cb).not.toHaveBeenCalled()
  617. })
  618. // #7030
  619. it('should not fire on child component unmount w/ flush: pre', async () => {
  620. const visible = ref(true)
  621. const cb = vi.fn()
  622. const Parent = defineComponent({
  623. props: ['visible'],
  624. render() {
  625. return visible.value ? h(Comp) : null
  626. },
  627. })
  628. const Comp = {
  629. setup() {
  630. watch(visible, cb, { flush: 'pre' })
  631. },
  632. render() {},
  633. }
  634. const App = {
  635. render() {
  636. return h(Parent, {
  637. visible: visible.value,
  638. })
  639. },
  640. }
  641. render(h(App), nodeOps.createElement('div'))
  642. expect(cb).not.toHaveBeenCalled()
  643. visible.value = false
  644. await nextTick()
  645. expect(cb).not.toHaveBeenCalled()
  646. })
  647. // #7030
  648. it('flush: pre watcher in child component should not fire before parent update', async () => {
  649. const b = ref(0)
  650. const calls: string[] = []
  651. const Comp = {
  652. setup() {
  653. watch(
  654. () => b.value,
  655. val => {
  656. calls.push('watcher child')
  657. },
  658. { flush: 'pre' },
  659. )
  660. return () => {
  661. b.value
  662. calls.push('render child')
  663. }
  664. },
  665. }
  666. const Parent = {
  667. props: ['a'],
  668. setup() {
  669. watch(
  670. () => b.value,
  671. val => {
  672. calls.push('watcher parent')
  673. },
  674. { flush: 'pre' },
  675. )
  676. return () => {
  677. b.value
  678. calls.push('render parent')
  679. return h(Comp)
  680. }
  681. },
  682. }
  683. const App = {
  684. render() {
  685. return h(Parent, {
  686. a: b.value,
  687. })
  688. },
  689. }
  690. render(h(App), nodeOps.createElement('div'))
  691. expect(calls).toEqual(['render parent', 'render child'])
  692. b.value++
  693. await nextTick()
  694. expect(calls).toEqual([
  695. 'render parent',
  696. 'render child',
  697. 'watcher parent',
  698. 'render parent',
  699. 'watcher child',
  700. 'render child',
  701. ])
  702. })
  703. // #1763
  704. it('flush: pre watcher watching props should fire before child update', async () => {
  705. const a = ref(0)
  706. const b = ref(0)
  707. const c = ref(0)
  708. const calls: string[] = []
  709. const Comp = {
  710. props: ['a', 'b'],
  711. setup(props: any) {
  712. watch(
  713. () => props.a + props.b,
  714. () => {
  715. calls.push('watcher 1')
  716. c.value++
  717. },
  718. { flush: 'pre' },
  719. )
  720. // #1777 chained pre-watcher
  721. watch(
  722. c,
  723. () => {
  724. calls.push('watcher 2')
  725. },
  726. { flush: 'pre' },
  727. )
  728. return () => {
  729. c.value
  730. calls.push('render')
  731. }
  732. },
  733. }
  734. const App = {
  735. render() {
  736. return h(Comp, { a: a.value, b: b.value })
  737. },
  738. }
  739. render(h(App), nodeOps.createElement('div'))
  740. expect(calls).toEqual(['render'])
  741. // both props are updated
  742. // should trigger pre-flush watcher first and only once
  743. // then trigger child render
  744. a.value++
  745. b.value++
  746. await nextTick()
  747. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  748. })
  749. // #5721
  750. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  751. const count = ref(0)
  752. const calls: string[] = []
  753. const App = {
  754. render() {},
  755. setup() {
  756. watch(
  757. count,
  758. () => {
  759. calls.push('watch ' + count.value)
  760. },
  761. { flush: 'pre' },
  762. )
  763. onMounted(() => {
  764. calls.push('mounted')
  765. })
  766. // mutate multiple times
  767. count.value++
  768. count.value++
  769. count.value++
  770. },
  771. }
  772. render(h(App), nodeOps.createElement('div'))
  773. expect(calls).toMatchObject(['watch 3', 'mounted'])
  774. })
  775. // #1852
  776. it('flush: post watcher should fire after template refs updated', async () => {
  777. const toggle = ref(false)
  778. let dom: TestElement | null = null
  779. const App = {
  780. setup() {
  781. const domRef = ref<TestElement | null>(null)
  782. watch(
  783. toggle,
  784. () => {
  785. dom = domRef.value
  786. },
  787. { flush: 'post' },
  788. )
  789. return () => {
  790. return toggle.value ? h('p', { ref: domRef }) : null
  791. }
  792. },
  793. }
  794. render(h(App), nodeOps.createElement('div'))
  795. expect(dom).toBe(null)
  796. toggle.value = true
  797. await nextTick()
  798. expect(dom!.tag).toBe('p')
  799. })
  800. it('deep', async () => {
  801. const state = reactive({
  802. nested: {
  803. count: ref(0),
  804. },
  805. array: [1, 2, 3],
  806. map: new Map([
  807. ['a', 1],
  808. ['b', 2],
  809. ]),
  810. set: new Set([1, 2, 3]),
  811. })
  812. let dummy
  813. watch(
  814. () => state,
  815. state => {
  816. dummy = [
  817. state.nested.count,
  818. state.array[0],
  819. state.map.get('a'),
  820. state.set.has(1),
  821. ]
  822. },
  823. { deep: true },
  824. )
  825. state.nested.count++
  826. await nextTick()
  827. expect(dummy).toEqual([1, 1, 1, true])
  828. // nested array mutation
  829. state.array[0] = 2
  830. await nextTick()
  831. expect(dummy).toEqual([1, 2, 1, true])
  832. // nested map mutation
  833. state.map.set('a', 2)
  834. await nextTick()
  835. expect(dummy).toEqual([1, 2, 2, true])
  836. // nested set mutation
  837. state.set.delete(1)
  838. await nextTick()
  839. expect(dummy).toEqual([1, 2, 2, false])
  840. })
  841. it('watching deep ref', async () => {
  842. const count = ref(0)
  843. const double = computed(() => count.value * 2)
  844. const state = reactive([count, double])
  845. let dummy
  846. watch(
  847. () => state,
  848. state => {
  849. dummy = [state[0].value, state[1].value]
  850. },
  851. { deep: true },
  852. )
  853. count.value++
  854. await nextTick()
  855. expect(dummy).toEqual([1, 2])
  856. })
  857. it('deep with symbols', async () => {
  858. const symbol1 = Symbol()
  859. const symbol2 = Symbol()
  860. const symbol3 = Symbol()
  861. const symbol4 = Symbol()
  862. const raw: any = {
  863. [symbol1]: {
  864. [symbol2]: 1,
  865. },
  866. }
  867. Object.defineProperty(raw, symbol3, {
  868. writable: true,
  869. enumerable: false,
  870. value: 1,
  871. })
  872. const state = reactive(raw)
  873. const spy = vi.fn()
  874. watch(() => state, spy, { deep: true })
  875. await nextTick()
  876. expect(spy).toHaveBeenCalledTimes(0)
  877. state[symbol1][symbol2] = 2
  878. await nextTick()
  879. expect(spy).toHaveBeenCalledTimes(1)
  880. // Non-enumerable properties don't trigger deep watchers
  881. state[symbol3] = 3
  882. await nextTick()
  883. expect(spy).toHaveBeenCalledTimes(1)
  884. // Adding a new symbol property
  885. state[symbol4] = 1
  886. await nextTick()
  887. expect(spy).toHaveBeenCalledTimes(2)
  888. // Removing a symbol property
  889. delete state[symbol4]
  890. await nextTick()
  891. expect(spy).toHaveBeenCalledTimes(3)
  892. })
  893. it('immediate', async () => {
  894. const count = ref(0)
  895. const cb = vi.fn()
  896. watch(count, cb, { immediate: true })
  897. expect(cb).toHaveBeenCalledTimes(1)
  898. count.value++
  899. await nextTick()
  900. expect(cb).toHaveBeenCalledTimes(2)
  901. })
  902. it('immediate: triggers when initial value is null', async () => {
  903. const state = ref(null)
  904. const spy = vi.fn()
  905. watch(() => state.value, spy, { immediate: true })
  906. expect(spy).toHaveBeenCalled()
  907. })
  908. it('immediate: triggers when initial value is undefined', async () => {
  909. const state = ref()
  910. const spy = vi.fn()
  911. watch(() => state.value, spy, { immediate: true })
  912. expect(spy).toHaveBeenCalledWith(undefined, undefined, expect.any(Function))
  913. state.value = 3
  914. await nextTick()
  915. expect(spy).toHaveBeenCalledTimes(2)
  916. // testing if undefined can trigger the watcher
  917. state.value = undefined
  918. await nextTick()
  919. expect(spy).toHaveBeenCalledTimes(3)
  920. // it shouldn't trigger if the same value is set
  921. state.value = undefined
  922. await nextTick()
  923. expect(spy).toHaveBeenCalledTimes(3)
  924. })
  925. it('warn immediate option when using effect', async () => {
  926. const count = ref(0)
  927. let dummy
  928. watchEffect(
  929. () => {
  930. dummy = count.value
  931. },
  932. // @ts-expect-error
  933. { immediate: false },
  934. )
  935. expect(dummy).toBe(0)
  936. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  937. count.value++
  938. await nextTick()
  939. expect(dummy).toBe(1)
  940. })
  941. it('warn and not respect deep option when using effect', async () => {
  942. const arr = ref([1, [2]])
  943. const spy = vi.fn()
  944. watchEffect(
  945. () => {
  946. spy()
  947. return arr
  948. },
  949. // @ts-expect-error
  950. { deep: true },
  951. )
  952. expect(spy).toHaveBeenCalledTimes(1)
  953. ;(arr.value[1] as Array<number>)[0] = 3
  954. await nextTick()
  955. expect(spy).toHaveBeenCalledTimes(1)
  956. expect(`"deep" option is only respected`).toHaveBeenWarned()
  957. })
  958. it('onTrack', async () => {
  959. const events: DebuggerEvent[] = []
  960. let dummy
  961. const onTrack = vi.fn((e: DebuggerEvent) => {
  962. events.push(e)
  963. })
  964. const obj = reactive({ foo: 1, bar: 2 })
  965. watchEffect(
  966. () => {
  967. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  968. },
  969. { onTrack },
  970. )
  971. await nextTick()
  972. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  973. expect(onTrack).toHaveBeenCalledTimes(3)
  974. expect(events).toMatchObject([
  975. {
  976. target: obj,
  977. type: TrackOpTypes.GET,
  978. key: 'foo',
  979. },
  980. {
  981. target: obj,
  982. type: TrackOpTypes.HAS,
  983. key: 'bar',
  984. },
  985. {
  986. target: obj,
  987. type: TrackOpTypes.ITERATE,
  988. key: ITERATE_KEY,
  989. },
  990. ])
  991. })
  992. it('onTrigger', async () => {
  993. const events: DebuggerEvent[] = []
  994. let dummy
  995. const onTrigger = vi.fn((e: DebuggerEvent) => {
  996. events.push(e)
  997. })
  998. const obj = reactive<{ foo?: number }>({ foo: 1 })
  999. watchEffect(
  1000. () => {
  1001. dummy = obj.foo
  1002. },
  1003. { onTrigger },
  1004. )
  1005. await nextTick()
  1006. expect(dummy).toBe(1)
  1007. obj.foo!++
  1008. await nextTick()
  1009. expect(dummy).toBe(2)
  1010. expect(onTrigger).toHaveBeenCalledTimes(1)
  1011. expect(events[0]).toMatchObject({
  1012. type: TriggerOpTypes.SET,
  1013. key: 'foo',
  1014. oldValue: 1,
  1015. newValue: 2,
  1016. })
  1017. delete obj.foo
  1018. await nextTick()
  1019. expect(dummy).toBeUndefined()
  1020. expect(onTrigger).toHaveBeenCalledTimes(2)
  1021. expect(events[1]).toMatchObject({
  1022. type: TriggerOpTypes.DELETE,
  1023. key: 'foo',
  1024. oldValue: 2,
  1025. })
  1026. })
  1027. it('should work sync', () => {
  1028. const v = ref(1)
  1029. let calls = 0
  1030. watch(
  1031. v,
  1032. () => {
  1033. ++calls
  1034. },
  1035. {
  1036. flush: 'sync',
  1037. },
  1038. )
  1039. expect(calls).toBe(0)
  1040. v.value++
  1041. expect(calls).toBe(1)
  1042. })
  1043. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  1044. const v = shallowRef({ a: 1 })
  1045. let sideEffect = 0
  1046. watch(v, obj => {
  1047. sideEffect = obj.a
  1048. })
  1049. v.value = v.value
  1050. await nextTick()
  1051. // should not trigger
  1052. expect(sideEffect).toBe(0)
  1053. v.value.a++
  1054. await nextTick()
  1055. // should not trigger
  1056. expect(sideEffect).toBe(0)
  1057. triggerRef(v)
  1058. await nextTick()
  1059. // should trigger now
  1060. expect(sideEffect).toBe(2)
  1061. })
  1062. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  1063. const v = shallowRef([] as any)
  1064. const spy = vi.fn()
  1065. watch([v], () => {
  1066. spy()
  1067. })
  1068. v.value.push(1)
  1069. triggerRef(v)
  1070. await nextTick()
  1071. // should trigger now
  1072. expect(spy).toHaveBeenCalledTimes(1)
  1073. })
  1074. test('should force trigger on triggerRef with toRef from reactive', async () => {
  1075. const foo = reactive({ bar: 1 })
  1076. const bar = toRef(foo, 'bar')
  1077. const spy = vi.fn()
  1078. watchEffect(() => {
  1079. bar.value
  1080. spy()
  1081. })
  1082. expect(spy).toHaveBeenCalledTimes(1)
  1083. triggerRef(bar)
  1084. await nextTick()
  1085. // should trigger now
  1086. expect(spy).toHaveBeenCalledTimes(2)
  1087. })
  1088. // #2125
  1089. test('watchEffect should not recursively trigger itself', async () => {
  1090. const spy = vi.fn()
  1091. const price = ref(10)
  1092. const history = ref<number[]>([])
  1093. watchEffect(() => {
  1094. history.value.push(price.value)
  1095. spy()
  1096. })
  1097. await nextTick()
  1098. expect(spy).toHaveBeenCalledTimes(1)
  1099. })
  1100. // #2231
  1101. test('computed refs should not trigger watch if value has no change', async () => {
  1102. const spy = vi.fn()
  1103. const source = ref(0)
  1104. const price = computed(() => source.value === 0)
  1105. watch(price, spy)
  1106. source.value++
  1107. await nextTick()
  1108. source.value++
  1109. await nextTick()
  1110. expect(spy).toHaveBeenCalledTimes(1)
  1111. })
  1112. // https://github.com/vuejs/core/issues/2381
  1113. test('$watch should always register its effects with its own instance', async () => {
  1114. let instance: ComponentInternalInstance | null
  1115. let _show: Ref<boolean>
  1116. const Child = defineComponent({
  1117. render: () => h('div'),
  1118. mounted() {
  1119. instance = getCurrentInstance()
  1120. },
  1121. unmounted() {},
  1122. })
  1123. const Comp = defineComponent({
  1124. setup() {
  1125. const comp = ref<ComponentPublicInstance | undefined>()
  1126. const show = ref(true)
  1127. _show = show
  1128. return { comp, show }
  1129. },
  1130. render() {
  1131. return this.show
  1132. ? h(Child, {
  1133. ref: vm => void (this.comp = vm as ComponentPublicInstance),
  1134. })
  1135. : null
  1136. },
  1137. mounted() {
  1138. // this call runs while Comp is currentInstance, but
  1139. // the effect for this `$watch` should nonetheless be registered with Child
  1140. this.comp!.$watch(
  1141. () => this.show,
  1142. () => void 0,
  1143. )
  1144. },
  1145. })
  1146. render(h(Comp), nodeOps.createElement('div'))
  1147. expect(instance!).toBeDefined()
  1148. expect(instance!.scope.effects).toBeInstanceOf(Array)
  1149. // includes the component's own render effect AND the watcher effect
  1150. expect(instance!.scope.effects.length).toBe(2)
  1151. _show!.value = false
  1152. await nextTick()
  1153. await nextTick()
  1154. expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
  1155. })
  1156. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  1157. let instance: any
  1158. const source = vi.fn()
  1159. const Comp = defineComponent({
  1160. render() {},
  1161. created(this: any) {
  1162. instance = this
  1163. this.$watch(source, function () {})
  1164. },
  1165. })
  1166. const root = nodeOps.createElement('div')
  1167. createApp(Comp).mount(root)
  1168. expect(instance).toBeDefined()
  1169. expect(source.mock.calls.some(args => args.includes(instance)))
  1170. })
  1171. test('should not leak `this.proxy` to setup()', () => {
  1172. const source = vi.fn()
  1173. const Comp = defineComponent({
  1174. render() {},
  1175. setup() {
  1176. watch(source, () => {})
  1177. },
  1178. })
  1179. const root = nodeOps.createElement('div')
  1180. createApp(Comp).mount(root)
  1181. // should not have any arguments
  1182. expect(source.mock.calls[0]).toMatchObject([])
  1183. })
  1184. // #2728
  1185. test('pre watcher callbacks should not track dependencies', async () => {
  1186. const a = ref(0)
  1187. const b = ref(0)
  1188. const updated = vi.fn()
  1189. const Child = defineComponent({
  1190. props: ['a'],
  1191. updated,
  1192. watch: {
  1193. a() {
  1194. b.value
  1195. },
  1196. },
  1197. render() {
  1198. return h('div', this.a)
  1199. },
  1200. })
  1201. const Parent = defineComponent({
  1202. render() {
  1203. return h(Child, { a: a.value })
  1204. },
  1205. })
  1206. const root = nodeOps.createElement('div')
  1207. createApp(Parent).mount(root)
  1208. a.value++
  1209. await nextTick()
  1210. expect(updated).toHaveBeenCalledTimes(1)
  1211. b.value++
  1212. await nextTick()
  1213. // should not track b as dependency of Child
  1214. expect(updated).toHaveBeenCalledTimes(1)
  1215. })
  1216. test('watching keypath', async () => {
  1217. const spy = vi.fn()
  1218. const Comp = defineComponent({
  1219. render() {},
  1220. data() {
  1221. return {
  1222. a: {
  1223. b: 1,
  1224. },
  1225. }
  1226. },
  1227. watch: {
  1228. 'a.b': spy,
  1229. },
  1230. created(this: any) {
  1231. this.$watch('a.b', spy)
  1232. },
  1233. mounted(this: any) {
  1234. this.a.b++
  1235. },
  1236. })
  1237. const root = nodeOps.createElement('div')
  1238. createApp(Comp).mount(root)
  1239. await nextTick()
  1240. expect(spy).toHaveBeenCalledTimes(2)
  1241. })
  1242. it('watching sources: ref<any[]>', async () => {
  1243. const foo = ref([1])
  1244. const spy = vi.fn()
  1245. watch(foo, () => {
  1246. spy()
  1247. })
  1248. foo.value = foo.value.slice()
  1249. await nextTick()
  1250. expect(spy).toBeCalledTimes(1)
  1251. })
  1252. it('watching multiple sources: computed', async () => {
  1253. let count = 0
  1254. const value = ref('1')
  1255. const plus = computed(() => !!value.value)
  1256. watch([plus], () => {
  1257. count++
  1258. })
  1259. value.value = '2'
  1260. await nextTick()
  1261. expect(plus.value).toBe(true)
  1262. expect(count).toBe(0)
  1263. })
  1264. // #4158
  1265. test('watch should not register in owner component if created inside detached scope', () => {
  1266. let instance: ComponentInternalInstance
  1267. const Comp = {
  1268. setup() {
  1269. instance = getCurrentInstance()!
  1270. effectScope(true).run(() => {
  1271. watch(
  1272. () => 1,
  1273. () => {},
  1274. )
  1275. })
  1276. return () => ''
  1277. },
  1278. }
  1279. const root = nodeOps.createElement('div')
  1280. createApp(Comp).mount(root)
  1281. // should not record watcher in detached scope and only the instance's
  1282. // own update effect
  1283. expect(instance!.scope.effects.length).toBe(1)
  1284. })
  1285. test('watchEffect should keep running if created in a detached scope', async () => {
  1286. const trigger = ref(0)
  1287. let countWE = 0
  1288. let countW = 0
  1289. const Comp = {
  1290. setup() {
  1291. effectScope(true).run(() => {
  1292. watchEffect(() => {
  1293. trigger.value
  1294. countWE++
  1295. })
  1296. watch(trigger, () => countW++)
  1297. })
  1298. return () => ''
  1299. },
  1300. }
  1301. const root = nodeOps.createElement('div')
  1302. render(h(Comp), root)
  1303. // only watchEffect as ran so far
  1304. expect(countWE).toBe(1)
  1305. expect(countW).toBe(0)
  1306. trigger.value++
  1307. await nextTick()
  1308. // both watchers run while component is mounted
  1309. expect(countWE).toBe(2)
  1310. expect(countW).toBe(1)
  1311. render(null, root) // unmount
  1312. await nextTick()
  1313. trigger.value++
  1314. await nextTick()
  1315. // both watchers run again event though component has been unmounted
  1316. expect(countWE).toBe(3)
  1317. expect(countW).toBe(2)
  1318. })
  1319. const options = [
  1320. { name: 'only trigger once watch' },
  1321. {
  1322. deep: true,
  1323. name: 'only trigger once watch with deep',
  1324. },
  1325. {
  1326. flush: 'sync',
  1327. name: 'only trigger once watch with flush: sync',
  1328. },
  1329. {
  1330. flush: 'pre',
  1331. name: 'only trigger once watch with flush: pre',
  1332. },
  1333. {
  1334. immediate: true,
  1335. name: 'only trigger once watch with immediate',
  1336. },
  1337. ] as const
  1338. test.each(options)('$name', async option => {
  1339. const count = ref(0)
  1340. const cb = vi.fn()
  1341. watch(count, cb, { once: true, ...option })
  1342. count.value++
  1343. await nextTick()
  1344. expect(count.value).toBe(1)
  1345. expect(cb).toHaveBeenCalledTimes(1)
  1346. count.value++
  1347. await nextTick()
  1348. expect(count.value).toBe(2)
  1349. expect(cb).toHaveBeenCalledTimes(1)
  1350. })
  1351. // #5151
  1352. test('OnCleanup also needs to be cleaned,', async () => {
  1353. const spy1 = vi.fn()
  1354. const spy2 = vi.fn()
  1355. const num = ref(0)
  1356. watch(num, (value, oldValue, onCleanup) => {
  1357. if (value > 1) {
  1358. return
  1359. }
  1360. spy1()
  1361. onCleanup(() => {
  1362. // OnCleanup also needs to be cleaned
  1363. spy2()
  1364. })
  1365. })
  1366. num.value++
  1367. await nextTick()
  1368. expect(spy1).toHaveBeenCalledTimes(1)
  1369. expect(spy2).toHaveBeenCalledTimes(0)
  1370. num.value++
  1371. await nextTick()
  1372. expect(spy1).toHaveBeenCalledTimes(1)
  1373. expect(spy2).toHaveBeenCalledTimes(1)
  1374. num.value++
  1375. await nextTick()
  1376. // would not be calld when value>1
  1377. expect(spy1).toHaveBeenCalledTimes(1)
  1378. expect(spy2).toHaveBeenCalledTimes(1)
  1379. })
  1380. test("effect should be removed from scope's effects after it is stopped", () => {
  1381. const num = ref(0)
  1382. let unwatch: () => void
  1383. let instance: ComponentInternalInstance
  1384. const Comp = {
  1385. setup() {
  1386. instance = getCurrentInstance()!
  1387. unwatch = watch(num, () => {
  1388. console.log(num.value)
  1389. })
  1390. return () => null
  1391. },
  1392. }
  1393. const root = nodeOps.createElement('div')
  1394. createApp(Comp).mount(root)
  1395. expect(instance!.scope.effects.length).toBe(2)
  1396. unwatch!()
  1397. expect(instance!.scope.effects.length).toBe(1)
  1398. const scope = effectScope()
  1399. scope.run(() => {
  1400. unwatch = watch(num, () => {
  1401. console.log(num.value)
  1402. })
  1403. })
  1404. expect(scope.effects.length).toBe(1)
  1405. unwatch!()
  1406. expect(scope.effects.length).toBe(0)
  1407. })
  1408. // simplified case of VueUse syncRef
  1409. test('sync watcher should not be batched', () => {
  1410. const a = ref(0)
  1411. const b = ref(0)
  1412. let pauseB = false
  1413. watch(
  1414. a,
  1415. () => {
  1416. pauseB = true
  1417. b.value = a.value + 1
  1418. pauseB = false
  1419. },
  1420. { flush: 'sync' },
  1421. )
  1422. watch(
  1423. b,
  1424. () => {
  1425. if (!pauseB) {
  1426. throw new Error('should not be called')
  1427. }
  1428. },
  1429. { flush: 'sync' },
  1430. )
  1431. a.value = 1
  1432. expect(b.value).toBe(2)
  1433. })
  1434. test('watchEffect should not fire on computed deps that did not change', async () => {
  1435. const a = ref(0)
  1436. const c = computed(() => a.value % 2)
  1437. const spy = vi.fn()
  1438. watchEffect(() => {
  1439. spy()
  1440. c.value
  1441. })
  1442. expect(spy).toHaveBeenCalledTimes(1)
  1443. a.value += 2
  1444. await nextTick()
  1445. expect(spy).toHaveBeenCalledTimes(1)
  1446. })
  1447. test('circular reference', async () => {
  1448. const obj = { a: 1 }
  1449. // @ts-expect-error
  1450. obj.b = obj
  1451. const foo = ref(obj)
  1452. const spy = vi.fn()
  1453. watch(foo, spy, { deep: true })
  1454. // @ts-expect-error
  1455. foo.value.b.a = 2
  1456. await nextTick()
  1457. expect(spy).toHaveBeenCalledTimes(1)
  1458. expect(foo.value.a).toBe(2)
  1459. })
  1460. test('watch immediate error in effect scope should be catched by onErrorCaptured', async () => {
  1461. const warn = vi.spyOn(console, 'warn')
  1462. warn.mockImplementation(() => {})
  1463. const ERROR_IN_SCOPE = 'ERROR_IN_SCOPE'
  1464. const ERROR_OUT_SCOPE = 'ERROR_OUT_SCOPE'
  1465. const errors = ref<string[]>([])
  1466. const Comp = {
  1467. setup() {
  1468. const trigger = ref(0)
  1469. effectScope(true).run(() => {
  1470. watch(
  1471. trigger,
  1472. () => {
  1473. throw new Error(ERROR_IN_SCOPE)
  1474. },
  1475. { immediate: true },
  1476. )
  1477. })
  1478. watchEffect(() => {
  1479. throw new Error(ERROR_OUT_SCOPE)
  1480. })
  1481. return () => ''
  1482. },
  1483. }
  1484. const root = nodeOps.createElement('div')
  1485. render(
  1486. h(
  1487. {
  1488. setup(_, { slots }) {
  1489. onErrorCaptured(e => {
  1490. errors.value.push(e.message)
  1491. return false
  1492. })
  1493. return () => h('div', slots.default && slots.default())
  1494. },
  1495. },
  1496. null,
  1497. () => [h(Comp)],
  1498. ),
  1499. root,
  1500. )
  1501. await nextTick()
  1502. // only watchEffect as ran so far
  1503. expect(errors.value).toHaveLength(2)
  1504. expect(errors.value[0]).toBe(ERROR_IN_SCOPE)
  1505. expect(errors.value[1]).toBe(ERROR_OUT_SCOPE)
  1506. warn.mockRestore()
  1507. })
  1508. })