apiWatch.spec.ts 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  1. import { vi } from 'vitest'
  2. import {
  3. watch,
  4. watchEffect,
  5. reactive,
  6. computed,
  7. nextTick,
  8. ref,
  9. defineComponent,
  10. getCurrentInstance,
  11. ComponentInternalInstance,
  12. ComponentPublicInstance
  13. } from '../src/index'
  14. import {
  15. render,
  16. nodeOps,
  17. serializeInner,
  18. TestElement,
  19. h,
  20. createApp,
  21. watchPostEffect,
  22. watchSyncEffect,
  23. onMounted
  24. } from '@vue/runtime-test'
  25. import {
  26. ITERATE_KEY,
  27. DebuggerEvent,
  28. TrackOpTypes,
  29. TriggerOpTypes,
  30. triggerRef,
  31. shallowRef,
  32. Ref,
  33. effectScope
  34. } from '@vue/reactivity'
  35. // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
  36. describe('api: watch', () => {
  37. it('effect', async () => {
  38. const state = reactive({ count: 0 })
  39. let dummy
  40. watchEffect(() => {
  41. dummy = state.count
  42. })
  43. expect(dummy).toBe(0)
  44. state.count++
  45. await nextTick()
  46. expect(dummy).toBe(1)
  47. })
  48. it('watching single source: getter', async () => {
  49. const state = reactive({ count: 0 })
  50. let dummy
  51. watch(
  52. () => state.count,
  53. (count, prevCount) => {
  54. dummy = [count, prevCount]
  55. // assert types
  56. count + 1
  57. if (prevCount) {
  58. prevCount + 1
  59. }
  60. }
  61. )
  62. state.count++
  63. await nextTick()
  64. expect(dummy).toMatchObject([1, 0])
  65. })
  66. it('watching single source: ref', async () => {
  67. const count = ref(0)
  68. let dummy
  69. watch(count, (count, prevCount) => {
  70. dummy = [count, prevCount]
  71. // assert types
  72. count + 1
  73. if (prevCount) {
  74. prevCount + 1
  75. }
  76. })
  77. count.value++
  78. await nextTick()
  79. expect(dummy).toMatchObject([1, 0])
  80. })
  81. it('watching single source: array', async () => {
  82. const array = reactive([] as number[])
  83. const spy = vi.fn()
  84. watch(array, spy)
  85. array.push(1)
  86. await nextTick()
  87. expect(spy).toBeCalledTimes(1)
  88. expect(spy).toBeCalledWith([1], expect.anything(), expect.anything())
  89. })
  90. it('should not fire if watched getter result did not change', async () => {
  91. const spy = vi.fn()
  92. const n = ref(0)
  93. watch(() => n.value % 2, spy)
  94. n.value++
  95. await nextTick()
  96. expect(spy).toBeCalledTimes(1)
  97. n.value += 2
  98. await nextTick()
  99. // should not be called again because getter result did not change
  100. expect(spy).toBeCalledTimes(1)
  101. })
  102. it('watching single source: computed ref', async () => {
  103. const count = ref(0)
  104. const plus = computed(() => count.value + 1)
  105. let dummy
  106. watch(plus, (count, prevCount) => {
  107. dummy = [count, prevCount]
  108. // assert types
  109. count + 1
  110. if (prevCount) {
  111. prevCount + 1
  112. }
  113. })
  114. count.value++
  115. await nextTick()
  116. expect(dummy).toMatchObject([2, 1])
  117. })
  118. it('watching primitive with deep: true', async () => {
  119. const count = ref(0)
  120. let dummy
  121. watch(
  122. count,
  123. (c, prevCount) => {
  124. dummy = [c, prevCount]
  125. },
  126. {
  127. deep: true
  128. }
  129. )
  130. count.value++
  131. await nextTick()
  132. expect(dummy).toMatchObject([1, 0])
  133. })
  134. it('directly watching reactive object (with automatic deep: true)', async () => {
  135. const src = reactive({
  136. count: 0
  137. })
  138. let dummy
  139. watch(src, ({ count }) => {
  140. dummy = count
  141. })
  142. src.count++
  143. await nextTick()
  144. expect(dummy).toBe(1)
  145. })
  146. it('watching multiple sources', async () => {
  147. const state = reactive({ count: 1 })
  148. const count = ref(1)
  149. const plus = computed(() => count.value + 1)
  150. let dummy
  151. watch([() => state.count, count, plus], (vals, oldVals) => {
  152. dummy = [vals, oldVals]
  153. // assert types
  154. vals.concat(1)
  155. oldVals.concat(1)
  156. })
  157. state.count++
  158. count.value++
  159. await nextTick()
  160. expect(dummy).toMatchObject([
  161. [2, 2, 3],
  162. [1, 1, 2]
  163. ])
  164. })
  165. it('watching multiple sources: undefined initial values and immediate: true', async () => {
  166. const a = ref()
  167. const b = ref()
  168. let called = false
  169. watch(
  170. [a, b],
  171. ([newA, newB], [oldA, oldB]) => {
  172. called = true
  173. expect([newA, newB]).toMatchObject([undefined, undefined])
  174. expect([oldA, oldB]).toMatchObject([undefined, undefined])
  175. },
  176. { immediate: true }
  177. )
  178. await nextTick()
  179. expect(called).toBe(true)
  180. })
  181. it('watching multiple sources: readonly array', async () => {
  182. const state = reactive({ count: 1 })
  183. const status = ref(false)
  184. let dummy
  185. watch([() => state.count, status] as const, (vals, oldVals) => {
  186. dummy = [vals, oldVals]
  187. const [count] = vals
  188. const [, oldStatus] = oldVals
  189. // assert types
  190. count + 1
  191. oldStatus === true
  192. })
  193. state.count++
  194. status.value = true
  195. await nextTick()
  196. expect(dummy).toMatchObject([
  197. [2, true],
  198. [1, false]
  199. ])
  200. })
  201. it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
  202. const src = reactive({ count: 0 })
  203. let dummy
  204. watch([src], ([state]) => {
  205. dummy = state
  206. // assert types
  207. state.count === 1
  208. })
  209. src.count++
  210. await nextTick()
  211. expect(dummy).toMatchObject({ count: 1 })
  212. })
  213. it('warn invalid watch source', () => {
  214. // @ts-expect-error
  215. watch(1, () => {})
  216. expect(`Invalid watch source`).toHaveBeenWarned()
  217. })
  218. it('warn invalid watch source: multiple sources', () => {
  219. watch([1], () => {})
  220. expect(`Invalid watch source`).toHaveBeenWarned()
  221. })
  222. it('stopping the watcher (effect)', async () => {
  223. const state = reactive({ count: 0 })
  224. let dummy
  225. const stop = watchEffect(() => {
  226. dummy = state.count
  227. })
  228. expect(dummy).toBe(0)
  229. stop()
  230. state.count++
  231. await nextTick()
  232. // should not update
  233. expect(dummy).toBe(0)
  234. })
  235. it('stopping the watcher (with source)', async () => {
  236. const state = reactive({ count: 0 })
  237. let dummy
  238. const stop = watch(
  239. () => state.count,
  240. count => {
  241. dummy = count
  242. }
  243. )
  244. state.count++
  245. await nextTick()
  246. expect(dummy).toBe(1)
  247. stop()
  248. state.count++
  249. await nextTick()
  250. // should not update
  251. expect(dummy).toBe(1)
  252. })
  253. it('cleanup registration (effect)', async () => {
  254. const state = reactive({ count: 0 })
  255. const cleanup = vi.fn()
  256. let dummy
  257. const stop = watchEffect(onCleanup => {
  258. onCleanup(cleanup)
  259. dummy = state.count
  260. })
  261. expect(dummy).toBe(0)
  262. state.count++
  263. await nextTick()
  264. expect(cleanup).toHaveBeenCalledTimes(1)
  265. expect(dummy).toBe(1)
  266. stop()
  267. expect(cleanup).toHaveBeenCalledTimes(2)
  268. })
  269. it('cleanup registration (with source)', async () => {
  270. const count = ref(0)
  271. const cleanup = vi.fn()
  272. let dummy
  273. const stop = watch(count, (count, prevCount, onCleanup) => {
  274. onCleanup(cleanup)
  275. dummy = count
  276. })
  277. count.value++
  278. await nextTick()
  279. expect(cleanup).toHaveBeenCalledTimes(0)
  280. expect(dummy).toBe(1)
  281. count.value++
  282. await nextTick()
  283. expect(cleanup).toHaveBeenCalledTimes(1)
  284. expect(dummy).toBe(2)
  285. stop()
  286. expect(cleanup).toHaveBeenCalledTimes(2)
  287. })
  288. it('flush timing: pre (default)', async () => {
  289. const count = ref(0)
  290. const count2 = ref(0)
  291. let callCount = 0
  292. let result1
  293. let result2
  294. const assertion = vi.fn((count, count2Value) => {
  295. callCount++
  296. // on mount, the watcher callback should be called before DOM render
  297. // on update, should be called before the count is updated
  298. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  299. result1 = serializeInner(root) === expectedDOM
  300. // in a pre-flush callback, all state should have been updated
  301. const expectedState = callCount - 1
  302. result2 = count === expectedState && count2Value === expectedState
  303. })
  304. const Comp = {
  305. setup() {
  306. watchEffect(() => {
  307. assertion(count.value, count2.value)
  308. })
  309. return () => count.value
  310. }
  311. }
  312. const root = nodeOps.createElement('div')
  313. render(h(Comp), root)
  314. expect(assertion).toHaveBeenCalledTimes(1)
  315. expect(result1).toBe(true)
  316. expect(result2).toBe(true)
  317. count.value++
  318. count2.value++
  319. await nextTick()
  320. // two mutations should result in 1 callback execution
  321. expect(assertion).toHaveBeenCalledTimes(2)
  322. expect(result1).toBe(true)
  323. expect(result2).toBe(true)
  324. })
  325. it('flush timing: post', async () => {
  326. const count = ref(0)
  327. let result
  328. const assertion = vi.fn(count => {
  329. result = serializeInner(root) === `${count}`
  330. })
  331. const Comp = {
  332. setup() {
  333. watchEffect(
  334. () => {
  335. assertion(count.value)
  336. },
  337. { flush: 'post' }
  338. )
  339. return () => count.value
  340. }
  341. }
  342. const root = nodeOps.createElement('div')
  343. render(h(Comp), root)
  344. expect(assertion).toHaveBeenCalledTimes(1)
  345. expect(result).toBe(true)
  346. count.value++
  347. await nextTick()
  348. expect(assertion).toHaveBeenCalledTimes(2)
  349. expect(result).toBe(true)
  350. })
  351. it('watchPostEffect', async () => {
  352. const count = ref(0)
  353. let result
  354. const assertion = vi.fn(count => {
  355. result = serializeInner(root) === `${count}`
  356. })
  357. const Comp = {
  358. setup() {
  359. watchPostEffect(() => {
  360. assertion(count.value)
  361. })
  362. return () => count.value
  363. }
  364. }
  365. const root = nodeOps.createElement('div')
  366. render(h(Comp), root)
  367. expect(assertion).toHaveBeenCalledTimes(1)
  368. expect(result).toBe(true)
  369. count.value++
  370. await nextTick()
  371. expect(assertion).toHaveBeenCalledTimes(2)
  372. expect(result).toBe(true)
  373. })
  374. it('flush timing: sync', async () => {
  375. const count = ref(0)
  376. const count2 = ref(0)
  377. let callCount = 0
  378. let result1
  379. let result2
  380. const assertion = vi.fn(count => {
  381. callCount++
  382. // on mount, the watcher callback should be called before DOM render
  383. // on update, should be called before the count is updated
  384. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  385. result1 = serializeInner(root) === expectedDOM
  386. // in a sync callback, state mutation on the next line should not have
  387. // executed yet on the 2nd call, but will be on the 3rd call.
  388. const expectedState = callCount < 3 ? 0 : 1
  389. result2 = count2.value === expectedState
  390. })
  391. const Comp = {
  392. setup() {
  393. watchEffect(
  394. () => {
  395. assertion(count.value)
  396. },
  397. {
  398. flush: 'sync'
  399. }
  400. )
  401. return () => count.value
  402. }
  403. }
  404. const root = nodeOps.createElement('div')
  405. render(h(Comp), root)
  406. expect(assertion).toHaveBeenCalledTimes(1)
  407. expect(result1).toBe(true)
  408. expect(result2).toBe(true)
  409. count.value++
  410. count2.value++
  411. await nextTick()
  412. expect(assertion).toHaveBeenCalledTimes(3)
  413. expect(result1).toBe(true)
  414. expect(result2).toBe(true)
  415. })
  416. it('watchSyncEffect', async () => {
  417. const count = ref(0)
  418. const count2 = ref(0)
  419. let callCount = 0
  420. let result1
  421. let result2
  422. const assertion = vi.fn(count => {
  423. callCount++
  424. // on mount, the watcher callback should be called before DOM render
  425. // on update, should be called before the count is updated
  426. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  427. result1 = serializeInner(root) === expectedDOM
  428. // in a sync callback, state mutation on the next line should not have
  429. // executed yet on the 2nd call, but will be on the 3rd call.
  430. const expectedState = callCount < 3 ? 0 : 1
  431. result2 = count2.value === expectedState
  432. })
  433. const Comp = {
  434. setup() {
  435. watchSyncEffect(() => {
  436. assertion(count.value)
  437. })
  438. return () => count.value
  439. }
  440. }
  441. const root = nodeOps.createElement('div')
  442. render(h(Comp), root)
  443. expect(assertion).toHaveBeenCalledTimes(1)
  444. expect(result1).toBe(true)
  445. expect(result2).toBe(true)
  446. count.value++
  447. count2.value++
  448. await nextTick()
  449. expect(assertion).toHaveBeenCalledTimes(3)
  450. expect(result1).toBe(true)
  451. expect(result2).toBe(true)
  452. })
  453. it('should not fire on component unmount w/ flush: post', async () => {
  454. const toggle = ref(true)
  455. const cb = vi.fn()
  456. const Comp = {
  457. setup() {
  458. watch(toggle, cb, { flush: 'post' })
  459. },
  460. render() {}
  461. }
  462. const App = {
  463. render() {
  464. return toggle.value ? h(Comp) : null
  465. }
  466. }
  467. render(h(App), nodeOps.createElement('div'))
  468. expect(cb).not.toHaveBeenCalled()
  469. toggle.value = false
  470. await nextTick()
  471. expect(cb).not.toHaveBeenCalled()
  472. })
  473. // #2291
  474. it('should not fire on component unmount w/ flush: pre', async () => {
  475. const toggle = ref(true)
  476. const cb = vi.fn()
  477. const Comp = {
  478. setup() {
  479. watch(toggle, cb, { flush: 'pre' })
  480. },
  481. render() {}
  482. }
  483. const App = {
  484. render() {
  485. return toggle.value ? h(Comp) : null
  486. }
  487. }
  488. render(h(App), nodeOps.createElement('div'))
  489. expect(cb).not.toHaveBeenCalled()
  490. toggle.value = false
  491. await nextTick()
  492. expect(cb).not.toHaveBeenCalled()
  493. })
  494. // #1763
  495. it('flush: pre watcher watching props should fire before child update', async () => {
  496. const a = ref(0)
  497. const b = ref(0)
  498. const c = ref(0)
  499. const calls: string[] = []
  500. const Comp = {
  501. props: ['a', 'b'],
  502. setup(props: any) {
  503. watch(
  504. () => props.a + props.b,
  505. () => {
  506. calls.push('watcher 1')
  507. c.value++
  508. },
  509. { flush: 'pre' }
  510. )
  511. // #1777 chained pre-watcher
  512. watch(
  513. c,
  514. () => {
  515. calls.push('watcher 2')
  516. },
  517. { flush: 'pre' }
  518. )
  519. return () => {
  520. c.value
  521. calls.push('render')
  522. }
  523. }
  524. }
  525. const App = {
  526. render() {
  527. return h(Comp, { a: a.value, b: b.value })
  528. }
  529. }
  530. render(h(App), nodeOps.createElement('div'))
  531. expect(calls).toEqual(['render'])
  532. // both props are updated
  533. // should trigger pre-flush watcher first and only once
  534. // then trigger child render
  535. a.value++
  536. b.value++
  537. await nextTick()
  538. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  539. })
  540. // #5721
  541. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  542. const count = ref(0)
  543. const calls: string[] = []
  544. const App = {
  545. render() {},
  546. setup() {
  547. watch(
  548. count,
  549. () => {
  550. calls.push('watch ' + count.value)
  551. },
  552. { flush: 'pre' }
  553. )
  554. onMounted(() => {
  555. calls.push('mounted')
  556. })
  557. // mutate multiple times
  558. count.value++
  559. count.value++
  560. count.value++
  561. }
  562. }
  563. render(h(App), nodeOps.createElement('div'))
  564. expect(calls).toMatchObject(['watch 3', 'mounted'])
  565. })
  566. // #1852
  567. it('flush: post watcher should fire after template refs updated', async () => {
  568. const toggle = ref(false)
  569. let dom: TestElement | null = null
  570. const App = {
  571. setup() {
  572. const domRef = ref<TestElement | null>(null)
  573. watch(
  574. toggle,
  575. () => {
  576. dom = domRef.value
  577. },
  578. { flush: 'post' }
  579. )
  580. return () => {
  581. return toggle.value ? h('p', { ref: domRef }) : null
  582. }
  583. }
  584. }
  585. render(h(App), nodeOps.createElement('div'))
  586. expect(dom).toBe(null)
  587. toggle.value = true
  588. await nextTick()
  589. expect(dom!.tag).toBe('p')
  590. })
  591. it('deep', async () => {
  592. const state = reactive({
  593. nested: {
  594. count: ref(0)
  595. },
  596. array: [1, 2, 3],
  597. map: new Map([
  598. ['a', 1],
  599. ['b', 2]
  600. ]),
  601. set: new Set([1, 2, 3])
  602. })
  603. let dummy
  604. watch(
  605. () => state,
  606. state => {
  607. dummy = [
  608. state.nested.count,
  609. state.array[0],
  610. state.map.get('a'),
  611. state.set.has(1)
  612. ]
  613. },
  614. { deep: true }
  615. )
  616. state.nested.count++
  617. await nextTick()
  618. expect(dummy).toEqual([1, 1, 1, true])
  619. // nested array mutation
  620. state.array[0] = 2
  621. await nextTick()
  622. expect(dummy).toEqual([1, 2, 1, true])
  623. // nested map mutation
  624. state.map.set('a', 2)
  625. await nextTick()
  626. expect(dummy).toEqual([1, 2, 2, true])
  627. // nested set mutation
  628. state.set.delete(1)
  629. await nextTick()
  630. expect(dummy).toEqual([1, 2, 2, false])
  631. })
  632. it('watching deep ref', async () => {
  633. const count = ref(0)
  634. const double = computed(() => count.value * 2)
  635. const state = reactive([count, double])
  636. let dummy
  637. watch(
  638. () => state,
  639. state => {
  640. dummy = [state[0].value, state[1].value]
  641. },
  642. { deep: true }
  643. )
  644. count.value++
  645. await nextTick()
  646. expect(dummy).toEqual([1, 2])
  647. })
  648. it('immediate', async () => {
  649. const count = ref(0)
  650. const cb = vi.fn()
  651. watch(count, cb, { immediate: true })
  652. expect(cb).toHaveBeenCalledTimes(1)
  653. count.value++
  654. await nextTick()
  655. expect(cb).toHaveBeenCalledTimes(2)
  656. })
  657. it('immediate: triggers when initial value is null', async () => {
  658. const state = ref(null)
  659. const spy = vi.fn()
  660. watch(() => state.value, spy, { immediate: true })
  661. expect(spy).toHaveBeenCalled()
  662. })
  663. it('immediate: triggers when initial value is undefined', async () => {
  664. const state = ref()
  665. const spy = vi.fn()
  666. watch(() => state.value, spy, { immediate: true })
  667. expect(spy).toHaveBeenCalledWith(undefined, undefined, expect.any(Function))
  668. state.value = 3
  669. await nextTick()
  670. expect(spy).toHaveBeenCalledTimes(2)
  671. // testing if undefined can trigger the watcher
  672. state.value = undefined
  673. await nextTick()
  674. expect(spy).toHaveBeenCalledTimes(3)
  675. // it shouldn't trigger if the same value is set
  676. state.value = undefined
  677. await nextTick()
  678. expect(spy).toHaveBeenCalledTimes(3)
  679. })
  680. it('warn immediate option when using effect', async () => {
  681. const count = ref(0)
  682. let dummy
  683. watchEffect(
  684. () => {
  685. dummy = count.value
  686. },
  687. // @ts-expect-error
  688. { immediate: false }
  689. )
  690. expect(dummy).toBe(0)
  691. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  692. count.value++
  693. await nextTick()
  694. expect(dummy).toBe(1)
  695. })
  696. it('warn and not respect deep option when using effect', async () => {
  697. const arr = ref([1, [2]])
  698. const spy = vi.fn()
  699. watchEffect(
  700. () => {
  701. spy()
  702. return arr
  703. },
  704. // @ts-expect-error
  705. { deep: true }
  706. )
  707. expect(spy).toHaveBeenCalledTimes(1)
  708. ;(arr.value[1] as Array<number>)[0] = 3
  709. await nextTick()
  710. expect(spy).toHaveBeenCalledTimes(1)
  711. expect(`"deep" option is only respected`).toHaveBeenWarned()
  712. })
  713. it('onTrack', async () => {
  714. const events: DebuggerEvent[] = []
  715. let dummy
  716. const onTrack = vi.fn((e: DebuggerEvent) => {
  717. events.push(e)
  718. })
  719. const obj = reactive({ foo: 1, bar: 2 })
  720. watchEffect(
  721. () => {
  722. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  723. },
  724. { onTrack }
  725. )
  726. await nextTick()
  727. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  728. expect(onTrack).toHaveBeenCalledTimes(3)
  729. expect(events).toMatchObject([
  730. {
  731. target: obj,
  732. type: TrackOpTypes.GET,
  733. key: 'foo'
  734. },
  735. {
  736. target: obj,
  737. type: TrackOpTypes.HAS,
  738. key: 'bar'
  739. },
  740. {
  741. target: obj,
  742. type: TrackOpTypes.ITERATE,
  743. key: ITERATE_KEY
  744. }
  745. ])
  746. })
  747. it('onTrigger', async () => {
  748. const events: DebuggerEvent[] = []
  749. let dummy
  750. const onTrigger = vi.fn((e: DebuggerEvent) => {
  751. events.push(e)
  752. })
  753. const obj = reactive<{ foo?: number }>({ foo: 1 })
  754. watchEffect(
  755. () => {
  756. dummy = obj.foo
  757. },
  758. { onTrigger }
  759. )
  760. await nextTick()
  761. expect(dummy).toBe(1)
  762. obj.foo!++
  763. await nextTick()
  764. expect(dummy).toBe(2)
  765. expect(onTrigger).toHaveBeenCalledTimes(1)
  766. expect(events[0]).toMatchObject({
  767. type: TriggerOpTypes.SET,
  768. key: 'foo',
  769. oldValue: 1,
  770. newValue: 2
  771. })
  772. delete obj.foo
  773. await nextTick()
  774. expect(dummy).toBeUndefined()
  775. expect(onTrigger).toHaveBeenCalledTimes(2)
  776. expect(events[1]).toMatchObject({
  777. type: TriggerOpTypes.DELETE,
  778. key: 'foo',
  779. oldValue: 2
  780. })
  781. })
  782. it('should work sync', () => {
  783. const v = ref(1)
  784. let calls = 0
  785. watch(
  786. v,
  787. () => {
  788. ++calls
  789. },
  790. {
  791. flush: 'sync'
  792. }
  793. )
  794. expect(calls).toBe(0)
  795. v.value++
  796. expect(calls).toBe(1)
  797. })
  798. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  799. const v = shallowRef({ a: 1 })
  800. let sideEffect = 0
  801. watch(v, obj => {
  802. sideEffect = obj.a
  803. })
  804. v.value = v.value
  805. await nextTick()
  806. // should not trigger
  807. expect(sideEffect).toBe(0)
  808. v.value.a++
  809. await nextTick()
  810. // should not trigger
  811. expect(sideEffect).toBe(0)
  812. triggerRef(v)
  813. await nextTick()
  814. // should trigger now
  815. expect(sideEffect).toBe(2)
  816. })
  817. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  818. const v = shallowRef([] as any)
  819. const spy = vi.fn()
  820. watch([v], () => {
  821. spy()
  822. })
  823. v.value.push(1)
  824. triggerRef(v)
  825. await nextTick()
  826. // should trigger now
  827. expect(spy).toHaveBeenCalledTimes(1)
  828. })
  829. // #2125
  830. test('watchEffect should not recursively trigger itself', async () => {
  831. const spy = vi.fn()
  832. const price = ref(10)
  833. const history = ref<number[]>([])
  834. watchEffect(() => {
  835. history.value.push(price.value)
  836. spy()
  837. })
  838. await nextTick()
  839. expect(spy).toHaveBeenCalledTimes(1)
  840. })
  841. // #2231
  842. test('computed refs should not trigger watch if value has no change', async () => {
  843. const spy = vi.fn()
  844. const source = ref(0)
  845. const price = computed(() => source.value === 0)
  846. watch(price, spy)
  847. source.value++
  848. await nextTick()
  849. source.value++
  850. await nextTick()
  851. expect(spy).toHaveBeenCalledTimes(1)
  852. })
  853. // https://github.com/vuejs/core/issues/2381
  854. test('$watch should always register its effects with its own instance', async () => {
  855. let instance: ComponentInternalInstance | null
  856. let _show: Ref<boolean>
  857. const Child = defineComponent({
  858. render: () => h('div'),
  859. mounted() {
  860. instance = getCurrentInstance()
  861. },
  862. unmounted() {}
  863. })
  864. const Comp = defineComponent({
  865. setup() {
  866. const comp = ref<ComponentPublicInstance | undefined>()
  867. const show = ref(true)
  868. _show = show
  869. return { comp, show }
  870. },
  871. render() {
  872. return this.show
  873. ? h(Child, {
  874. ref: vm => void (this.comp = vm as ComponentPublicInstance)
  875. })
  876. : null
  877. },
  878. mounted() {
  879. // this call runs while Comp is currentInstance, but
  880. // the effect for this `$watch` should nontheless be registered with Child
  881. this.comp!.$watch(
  882. () => this.show,
  883. () => void 0
  884. )
  885. }
  886. })
  887. render(h(Comp), nodeOps.createElement('div'))
  888. expect(instance!).toBeDefined()
  889. expect(instance!.scope.effects).toBeInstanceOf(Array)
  890. // includes the component's own render effect AND the watcher effect
  891. expect(instance!.scope.effects.length).toBe(2)
  892. _show!.value = false
  893. await nextTick()
  894. await nextTick()
  895. expect(instance!.scope.effects[0].active).toBe(false)
  896. })
  897. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  898. let instance: any
  899. const source = vi.fn()
  900. const Comp = defineComponent({
  901. render() {},
  902. created(this: any) {
  903. instance = this
  904. this.$watch(source, function () {})
  905. }
  906. })
  907. const root = nodeOps.createElement('div')
  908. createApp(Comp).mount(root)
  909. expect(instance).toBeDefined()
  910. expect(source).toHaveBeenCalledWith(instance)
  911. })
  912. test('should not leak `this.proxy` to setup()', () => {
  913. const source = vi.fn()
  914. const Comp = defineComponent({
  915. render() {},
  916. setup() {
  917. watch(source, () => {})
  918. }
  919. })
  920. const root = nodeOps.createElement('div')
  921. createApp(Comp).mount(root)
  922. // should not have any arguments
  923. expect(source.mock.calls[0]).toMatchObject([])
  924. })
  925. // #2728
  926. test('pre watcher callbacks should not track dependencies', async () => {
  927. const a = ref(0)
  928. const b = ref(0)
  929. const updated = vi.fn()
  930. const Child = defineComponent({
  931. props: ['a'],
  932. updated,
  933. watch: {
  934. a() {
  935. b.value
  936. }
  937. },
  938. render() {
  939. return h('div', this.a)
  940. }
  941. })
  942. const Parent = defineComponent({
  943. render() {
  944. return h(Child, { a: a.value })
  945. }
  946. })
  947. const root = nodeOps.createElement('div')
  948. createApp(Parent).mount(root)
  949. a.value++
  950. await nextTick()
  951. expect(updated).toHaveBeenCalledTimes(1)
  952. b.value++
  953. await nextTick()
  954. // should not track b as dependency of Child
  955. expect(updated).toHaveBeenCalledTimes(1)
  956. })
  957. test('watching keypath', async () => {
  958. const spy = vi.fn()
  959. const Comp = defineComponent({
  960. render() {},
  961. data() {
  962. return {
  963. a: {
  964. b: 1
  965. }
  966. }
  967. },
  968. watch: {
  969. 'a.b': spy
  970. },
  971. created(this: any) {
  972. this.$watch('a.b', spy)
  973. },
  974. mounted(this: any) {
  975. this.a.b++
  976. }
  977. })
  978. const root = nodeOps.createElement('div')
  979. createApp(Comp).mount(root)
  980. await nextTick()
  981. expect(spy).toHaveBeenCalledTimes(2)
  982. })
  983. it('watching sources: ref<any[]>', async () => {
  984. const foo = ref([1])
  985. const spy = vi.fn()
  986. watch(foo, () => {
  987. spy()
  988. })
  989. foo.value = foo.value.slice()
  990. await nextTick()
  991. expect(spy).toBeCalledTimes(1)
  992. })
  993. it('watching multiple sources: computed', async () => {
  994. let count = 0
  995. const value = ref('1')
  996. const plus = computed(() => !!value.value)
  997. watch([plus], () => {
  998. count++
  999. })
  1000. value.value = '2'
  1001. await nextTick()
  1002. expect(plus.value).toBe(true)
  1003. expect(count).toBe(0)
  1004. })
  1005. // #4158
  1006. test('watch should not register in owner component if created inside detached scope', () => {
  1007. let instance: ComponentInternalInstance
  1008. const Comp = {
  1009. setup() {
  1010. instance = getCurrentInstance()!
  1011. effectScope(true).run(() => {
  1012. watch(
  1013. () => 1,
  1014. () => {}
  1015. )
  1016. })
  1017. return () => ''
  1018. }
  1019. }
  1020. const root = nodeOps.createElement('div')
  1021. createApp(Comp).mount(root)
  1022. // should not record watcher in detached scope and only the instance's
  1023. // own update effect
  1024. expect(instance!.scope.effects.length).toBe(1)
  1025. })
  1026. test('watchEffect should keep running if created in a detatched scope', async () => {
  1027. const trigger = ref(0)
  1028. let countWE = 0
  1029. let countW = 0
  1030. const Comp = {
  1031. setup() {
  1032. effectScope(true).run(() => {
  1033. watchEffect(() => {
  1034. trigger.value
  1035. countWE++
  1036. })
  1037. watch(trigger, () => countW++)
  1038. })
  1039. return () => ''
  1040. }
  1041. }
  1042. const root = nodeOps.createElement('div')
  1043. render(h(Comp), root)
  1044. // only watchEffect as ran so far
  1045. expect(countWE).toBe(1)
  1046. expect(countW).toBe(0)
  1047. trigger.value++
  1048. await nextTick()
  1049. // both watchers run while component is mounted
  1050. expect(countWE).toBe(2)
  1051. expect(countW).toBe(1)
  1052. render(null, root) // unmount
  1053. await nextTick()
  1054. trigger.value++
  1055. await nextTick()
  1056. // both watchers run again event though component has been unmounted
  1057. expect(countWE).toBe(3)
  1058. expect(countW).toBe(2)
  1059. })
  1060. })