2
0

apiWatch.spec.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  1. import Vue from 'vue'
  2. import {
  3. watch,
  4. watchEffect,
  5. watchPostEffect,
  6. watchSyncEffect,
  7. reactive,
  8. computed,
  9. ref,
  10. triggerRef,
  11. shallowRef,
  12. h,
  13. onMounted,
  14. getCurrentInstance,
  15. effectScope,
  16. TrackOpTypes,
  17. TriggerOpTypes,
  18. DebuggerEvent
  19. } from 'v3'
  20. import { nextTick } from 'core/util'
  21. import { set } from 'core/observer'
  22. import { Component } from 'types/component'
  23. // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
  24. describe('api: watch', () => {
  25. it('effect', async () => {
  26. const state = reactive({ count: 0 })
  27. let dummy
  28. watchEffect(() => {
  29. dummy = state.count
  30. })
  31. expect(dummy).toBe(0)
  32. state.count++
  33. await nextTick()
  34. expect(dummy).toBe(1)
  35. })
  36. it('watching single source: getter', async () => {
  37. const state = reactive({ count: 0 })
  38. let dummy
  39. watch(
  40. () => state.count,
  41. (count, prevCount) => {
  42. dummy = [count, prevCount]
  43. // assert types
  44. count + 1
  45. if (prevCount) {
  46. prevCount + 1
  47. }
  48. }
  49. )
  50. state.count++
  51. await nextTick()
  52. expect(dummy).toMatchObject([1, 0])
  53. })
  54. it('watching single source: ref', async () => {
  55. const count = ref(0)
  56. let dummy
  57. watch(count, (count, prevCount) => {
  58. dummy = [count, prevCount]
  59. // assert types
  60. count + 1
  61. if (prevCount) {
  62. prevCount + 1
  63. }
  64. })
  65. count.value++
  66. await nextTick()
  67. expect(dummy).toMatchObject([1, 0])
  68. })
  69. it('watching single source: array', async () => {
  70. const array = reactive({ a: [] as number[] }).a
  71. const spy = vi.fn()
  72. watch(array, spy)
  73. array.push(1)
  74. await nextTick()
  75. expect(spy).toBeCalledTimes(1)
  76. expect(spy).toBeCalledWith([1], expect.anything(), expect.anything())
  77. })
  78. it('should not fire if watched getter result did not change', async () => {
  79. const spy = vi.fn()
  80. const n = ref(0)
  81. watch(() => n.value % 2, spy)
  82. n.value++
  83. await nextTick()
  84. expect(spy).toBeCalledTimes(1)
  85. n.value += 2
  86. await nextTick()
  87. // should not be called again because getter result did not change
  88. expect(spy).toBeCalledTimes(1)
  89. })
  90. it('watching single source: computed ref', async () => {
  91. const count = ref(0)
  92. const plus = computed(() => count.value + 1)
  93. let dummy
  94. watch(plus, (count, prevCount) => {
  95. dummy = [count, prevCount]
  96. // assert types
  97. count + 1
  98. if (prevCount) {
  99. prevCount + 1
  100. }
  101. })
  102. count.value++
  103. await nextTick()
  104. expect(dummy).toMatchObject([2, 1])
  105. })
  106. it('watching primitive with deep: true', async () => {
  107. const count = ref(0)
  108. let dummy
  109. watch(
  110. count,
  111. (c, prevCount) => {
  112. dummy = [c, prevCount]
  113. },
  114. {
  115. deep: true
  116. }
  117. )
  118. count.value++
  119. await nextTick()
  120. expect(dummy).toMatchObject([1, 0])
  121. })
  122. it('directly watching reactive object (with automatic deep: true)', async () => {
  123. const src = reactive({
  124. count: 0
  125. })
  126. let dummy
  127. watch(src, ({ count }) => {
  128. dummy = count
  129. })
  130. src.count++
  131. await nextTick()
  132. expect(dummy).toBe(1)
  133. })
  134. it('watching multiple sources', async () => {
  135. const state = reactive({ count: 1 })
  136. const count = ref(1)
  137. const plus = computed(() => count.value + 1)
  138. let dummy
  139. watch([() => state.count, count, plus], (vals, oldVals) => {
  140. dummy = [vals, oldVals]
  141. // assert types
  142. vals.concat(1)
  143. oldVals.concat(1)
  144. })
  145. state.count++
  146. count.value++
  147. await nextTick()
  148. expect(dummy).toMatchObject([
  149. [2, 2, 3],
  150. [1, 1, 2]
  151. ])
  152. })
  153. it('watching multiple sources: readonly array', async () => {
  154. const state = reactive({ count: 1 })
  155. const status = ref(false)
  156. let dummy
  157. watch([() => state.count, status] as const, (vals, oldVals) => {
  158. dummy = [vals, oldVals]
  159. const [count] = vals
  160. const [, oldStatus] = oldVals
  161. // assert types
  162. count + 1
  163. oldStatus === true
  164. })
  165. state.count++
  166. status.value = true
  167. await nextTick()
  168. expect(dummy).toMatchObject([
  169. [2, true],
  170. [1, false]
  171. ])
  172. })
  173. it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
  174. const src = reactive({ count: 0 })
  175. let dummy
  176. watch([src], ([state]) => {
  177. dummy = state
  178. // assert types
  179. state.count === 1
  180. })
  181. src.count++
  182. await nextTick()
  183. expect(dummy).toMatchObject({ count: 1 })
  184. })
  185. it('warn invalid watch source', () => {
  186. // @ts-expect-error
  187. watch(1, () => {})
  188. expect(`Invalid watch source`).toHaveBeenWarned()
  189. })
  190. it('warn invalid watch source: multiple sources', () => {
  191. watch([1], () => {})
  192. expect(`Invalid watch source`).toHaveBeenWarned()
  193. })
  194. it('stopping the watcher (effect)', async () => {
  195. const state = reactive({ count: 0 })
  196. let dummy
  197. const stop = watchEffect(() => {
  198. dummy = state.count
  199. })
  200. expect(dummy).toBe(0)
  201. stop()
  202. state.count++
  203. await nextTick()
  204. // should not update
  205. expect(dummy).toBe(0)
  206. })
  207. it('stopping the watcher (with source)', async () => {
  208. const state = reactive({ count: 0 })
  209. let dummy
  210. const stop = watch(
  211. () => state.count,
  212. count => {
  213. dummy = count
  214. }
  215. )
  216. state.count++
  217. await nextTick()
  218. expect(dummy).toBe(1)
  219. stop()
  220. state.count++
  221. await nextTick()
  222. // should not update
  223. expect(dummy).toBe(1)
  224. })
  225. it('cleanup registration (effect)', async () => {
  226. const state = reactive({ count: 0 })
  227. const cleanup = vi.fn()
  228. let dummy
  229. const stop = watchEffect(onCleanup => {
  230. onCleanup(cleanup)
  231. dummy = state.count
  232. })
  233. expect(dummy).toBe(0)
  234. state.count++
  235. await nextTick()
  236. expect(cleanup).toHaveBeenCalledTimes(1)
  237. expect(dummy).toBe(1)
  238. stop()
  239. expect(cleanup).toHaveBeenCalledTimes(2)
  240. })
  241. it('cleanup registration (with source)', async () => {
  242. const count = ref(0)
  243. const cleanup = vi.fn()
  244. let dummy
  245. const stop = watch(count, (count, prevCount, onCleanup) => {
  246. onCleanup(cleanup)
  247. dummy = count
  248. })
  249. count.value++
  250. await nextTick()
  251. expect(cleanup).toHaveBeenCalledTimes(0)
  252. expect(dummy).toBe(1)
  253. count.value++
  254. await nextTick()
  255. expect(cleanup).toHaveBeenCalledTimes(1)
  256. expect(dummy).toBe(2)
  257. stop()
  258. expect(cleanup).toHaveBeenCalledTimes(2)
  259. })
  260. it('flush timing: pre (default)', async () => {
  261. const count = ref(0)
  262. const count2 = ref(0)
  263. let callCount = 0
  264. let result1
  265. let result2
  266. const assertion = vi.fn((count, count2Value) => {
  267. callCount++
  268. // on mount, the watcher callback should be called before DOM render
  269. // on update, should be called before the count is updated
  270. const expectedDOM =
  271. callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
  272. result1 = container.innerHTML === expectedDOM
  273. // in a pre-flush callback, all state should have been updated
  274. const expectedState = callCount - 1
  275. result2 = count === expectedState && count2Value === expectedState
  276. })
  277. const Comp = {
  278. setup() {
  279. watchEffect(() => {
  280. assertion(count.value, count2.value)
  281. })
  282. return () => h('div', count.value)
  283. }
  284. }
  285. const container = document.createElement('div')
  286. const root = document.createElement('div')
  287. container.appendChild(root)
  288. new Vue(Comp).$mount(root)
  289. expect(assertion).toHaveBeenCalledTimes(1)
  290. expect(result1).toBe(true)
  291. expect(result2).toBe(true)
  292. count.value++
  293. count2.value++
  294. await nextTick()
  295. // two mutations should result in 1 callback execution
  296. expect(assertion).toHaveBeenCalledTimes(2)
  297. expect(result1).toBe(true)
  298. expect(result2).toBe(true)
  299. })
  300. // #12569
  301. it('flush:pre watcher triggered before component mount (in child components)', () => {
  302. const count = ref(0)
  303. const spy = vi.fn()
  304. const Comp = {
  305. setup() {
  306. watch(count, spy)
  307. count.value++
  308. return h => h('div')
  309. }
  310. }
  311. new Vue({
  312. render: h => h(Comp)
  313. }).$mount()
  314. expect(spy).toHaveBeenCalledTimes(1)
  315. })
  316. it('flush timing: post', async () => {
  317. const count = ref(0)
  318. let result
  319. const assertion = vi.fn(count => {
  320. result = container.innerHTML === `<div>${count}</div>`
  321. })
  322. const Comp = {
  323. setup() {
  324. watchEffect(
  325. () => {
  326. assertion(count.value)
  327. },
  328. { flush: 'post' }
  329. )
  330. return () => h('div', count.value)
  331. }
  332. }
  333. const container = document.createElement('div')
  334. const root = document.createElement('div')
  335. container.appendChild(root)
  336. new Vue(Comp).$mount(root)
  337. expect(assertion).toHaveBeenCalledTimes(1)
  338. expect(result).toBe(true)
  339. count.value++
  340. await nextTick()
  341. expect(assertion).toHaveBeenCalledTimes(2)
  342. expect(result).toBe(true)
  343. })
  344. it('watchPostEffect', async () => {
  345. const count = ref(0)
  346. let result
  347. const assertion = vi.fn(count => {
  348. result = container.innerHTML === `<div>${count}</div>`
  349. })
  350. const Comp = {
  351. setup() {
  352. watchPostEffect(() => {
  353. assertion(count.value)
  354. })
  355. return () => h('div', count.value)
  356. }
  357. }
  358. const container = document.createElement('div')
  359. const root = document.createElement('div')
  360. container.appendChild(root)
  361. new Vue(Comp).$mount(root)
  362. expect(assertion).toHaveBeenCalledTimes(1)
  363. expect(result).toBe(true)
  364. count.value++
  365. await nextTick()
  366. expect(assertion).toHaveBeenCalledTimes(2)
  367. expect(result).toBe(true)
  368. })
  369. it('flush timing: sync', async () => {
  370. const count = ref(0)
  371. const count2 = ref(0)
  372. let callCount = 0
  373. let result1
  374. let result2
  375. const assertion = vi.fn(count => {
  376. callCount++
  377. // on mount, the watcher callback should be called before DOM render
  378. // on update, should be called before the count is updated
  379. const expectedDOM =
  380. callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
  381. result1 = container.innerHTML === expectedDOM
  382. // in a sync callback, state mutation on the next line should not have
  383. // executed yet on the 2nd call, but will be on the 3rd call.
  384. const expectedState = callCount < 3 ? 0 : 1
  385. result2 = count2.value === expectedState
  386. })
  387. const Comp = {
  388. setup() {
  389. watchEffect(
  390. () => {
  391. assertion(count.value)
  392. },
  393. {
  394. flush: 'sync'
  395. }
  396. )
  397. return () => h('div', count.value)
  398. }
  399. }
  400. const container = document.createElement('div')
  401. const root = document.createElement('div')
  402. container.appendChild(root)
  403. new Vue(Comp).$mount(root)
  404. expect(assertion).toHaveBeenCalledTimes(1)
  405. expect(result1).toBe(true)
  406. expect(result2).toBe(true)
  407. count.value++
  408. count2.value++
  409. await nextTick()
  410. expect(assertion).toHaveBeenCalledTimes(3)
  411. expect(result1).toBe(true)
  412. expect(result2).toBe(true)
  413. })
  414. it('watchSyncEffect', async () => {
  415. const count = ref(0)
  416. const count2 = ref(0)
  417. let callCount = 0
  418. let result1
  419. let result2
  420. const assertion = vi.fn(count => {
  421. callCount++
  422. // on mount, the watcher callback should be called before DOM render
  423. // on update, should be called before the count is updated
  424. const expectedDOM =
  425. callCount === 1 ? `<div></div>` : `<div>${count - 1}</div>`
  426. result1 = container.innerHTML === expectedDOM
  427. // in a sync callback, state mutation on the next line should not have
  428. // executed yet on the 2nd call, but will be on the 3rd call.
  429. const expectedState = callCount < 3 ? 0 : 1
  430. result2 = count2.value === expectedState
  431. })
  432. const Comp = {
  433. setup() {
  434. watchSyncEffect(() => {
  435. assertion(count.value)
  436. })
  437. return () => h('div', count.value)
  438. }
  439. }
  440. const container = document.createElement('div')
  441. const root = document.createElement('div')
  442. container.appendChild(root)
  443. new Vue(Comp).$mount(root)
  444. expect(assertion).toHaveBeenCalledTimes(1)
  445. expect(result1).toBe(true)
  446. expect(result2).toBe(true)
  447. count.value++
  448. count2.value++
  449. await nextTick()
  450. expect(assertion).toHaveBeenCalledTimes(3)
  451. expect(result1).toBe(true)
  452. expect(result2).toBe(true)
  453. })
  454. it('should not fire on component unmount w/ flush: post', async () => {
  455. const toggle = ref(true)
  456. const cb = vi.fn()
  457. const Comp = {
  458. setup() {
  459. watch(toggle, cb, { flush: 'post' })
  460. },
  461. render() {}
  462. }
  463. const App = {
  464. render() {
  465. return toggle.value ? h(Comp) : null
  466. }
  467. }
  468. new Vue(App).$mount()
  469. expect(cb).not.toHaveBeenCalled()
  470. toggle.value = false
  471. await nextTick()
  472. expect(cb).not.toHaveBeenCalled()
  473. })
  474. it('should 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. new Vue(App).$mount()
  489. expect(cb).not.toHaveBeenCalled()
  490. toggle.value = false
  491. await nextTick()
  492. expect(cb).toHaveBeenCalledTimes(1)
  493. })
  494. // vuejs/core#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. // vuejs/core#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, { props: { a: a.value, b: b.value } })
  528. }
  529. }
  530. new Vue(App).$mount()
  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. // vuejs/core#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. new Vue(App).$mount()
  564. expect(calls).toMatchObject(['watch 3', 'mounted'])
  565. })
  566. // vuejs/core#1852
  567. it('flush: post watcher should fire after template refs updated', async () => {
  568. const toggle = ref(false)
  569. let dom: HTMLElement | null = null
  570. const App = {
  571. setup() {
  572. const domRef = ref<any>(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. new Vue(App).$mount()
  586. expect(dom).toBe(null)
  587. toggle.value = true
  588. await nextTick()
  589. expect(dom!.tagName).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])
  619. // nested array mutation
  620. set(state.array, 0, 2)
  621. await nextTick()
  622. expect(dummy).toEqual([1, 2])
  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.count, state.double]
  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).toHaveBeenCalled()
  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 })
  720. const r = ref(2)
  721. const c = computed(() => r.value + 1)
  722. watchEffect(
  723. () => {
  724. dummy = obj.foo + r.value + c.value
  725. },
  726. { onTrack }
  727. )
  728. await nextTick()
  729. expect(dummy).toEqual(6)
  730. expect(onTrack).toHaveBeenCalledTimes(3)
  731. expect(events).toMatchObject([
  732. {
  733. target: obj,
  734. type: TrackOpTypes.GET,
  735. key: 'foo'
  736. },
  737. {
  738. target: r,
  739. type: TrackOpTypes.GET,
  740. key: 'value'
  741. },
  742. {
  743. target: c,
  744. type: TrackOpTypes.GET,
  745. key: 'value'
  746. }
  747. ])
  748. })
  749. it('onTrigger', async () => {
  750. const events: DebuggerEvent[] = []
  751. let dummy
  752. const onTrigger = vi.fn((e: DebuggerEvent) => {
  753. events.push(e)
  754. })
  755. const obj = reactive<{
  756. foo: number
  757. bar: any[]
  758. baz: { qux?: number }
  759. }>({ foo: 1, bar: [], baz: {} })
  760. watchEffect(
  761. () => {
  762. dummy = obj.foo + (obj.bar[0] || 0) + (obj.baz.qux || 0)
  763. },
  764. { onTrigger }
  765. )
  766. await nextTick()
  767. expect(dummy).toBe(1)
  768. obj.foo++
  769. await nextTick()
  770. expect(dummy).toBe(2)
  771. expect(onTrigger).toHaveBeenCalledTimes(1)
  772. expect(events[0]).toMatchObject({
  773. type: TriggerOpTypes.SET,
  774. key: 'foo',
  775. target: obj,
  776. oldValue: 1,
  777. newValue: 2
  778. })
  779. obj.bar.push(1)
  780. await nextTick()
  781. expect(dummy).toBe(3)
  782. expect(onTrigger).toHaveBeenCalledTimes(2)
  783. expect(events[1]).toMatchObject({
  784. type: TriggerOpTypes.ARRAY_MUTATION,
  785. target: obj.bar,
  786. key: 'push'
  787. })
  788. set(obj.baz, 'qux', 1)
  789. await nextTick()
  790. expect(dummy).toBe(4)
  791. expect(onTrigger).toHaveBeenCalledTimes(3)
  792. expect(events[2]).toMatchObject({
  793. type: TriggerOpTypes.ADD,
  794. target: obj.baz,
  795. key: 'qux'
  796. })
  797. })
  798. it('should work sync', () => {
  799. const v = ref(1)
  800. let calls = 0
  801. watch(
  802. v,
  803. () => {
  804. ++calls
  805. },
  806. {
  807. flush: 'sync'
  808. }
  809. )
  810. expect(calls).toBe(0)
  811. v.value++
  812. expect(calls).toBe(1)
  813. })
  814. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  815. const v = shallowRef({ a: 1 })
  816. let sideEffect = 0
  817. watch(v, obj => {
  818. sideEffect = obj.a
  819. })
  820. v.value = v.value
  821. await nextTick()
  822. // should not trigger
  823. expect(sideEffect).toBe(0)
  824. v.value.a++
  825. await nextTick()
  826. // should not trigger
  827. expect(sideEffect).toBe(0)
  828. triggerRef(v)
  829. await nextTick()
  830. // should trigger now
  831. expect(sideEffect).toBe(2)
  832. })
  833. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  834. const v = shallowRef([] as any)
  835. const spy = vi.fn()
  836. watch([v], () => {
  837. spy()
  838. })
  839. v.value.push(1)
  840. triggerRef(v)
  841. await nextTick()
  842. // should trigger now
  843. expect(spy).toHaveBeenCalledTimes(1)
  844. })
  845. // vuejs/core#2125
  846. test('watchEffect should not recursively trigger itself', async () => {
  847. const spy = vi.fn()
  848. const price = ref(10)
  849. const history = ref<number[]>([])
  850. watchEffect(() => {
  851. history.value.push(price.value)
  852. spy()
  853. })
  854. await nextTick()
  855. expect(spy).toHaveBeenCalledTimes(1)
  856. })
  857. // vuejs/core#2231
  858. test('computed refs should not trigger watch if value has no change', async () => {
  859. const spy = vi.fn()
  860. const source = ref(0)
  861. const price = computed(() => source.value === 0)
  862. watch(price, spy)
  863. source.value++
  864. await nextTick()
  865. source.value++
  866. await nextTick()
  867. expect(spy).toHaveBeenCalledTimes(1)
  868. })
  869. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  870. let instance: any
  871. const source = vi.fn()
  872. const Comp = {
  873. render() {},
  874. created(this: any) {
  875. instance = this
  876. this.$watch(source, function () {})
  877. }
  878. }
  879. const root = document.createElement('div')
  880. new Vue(Comp).$mount(root)
  881. expect(instance).toBeDefined()
  882. expect(source).toHaveBeenCalledWith(instance)
  883. })
  884. test('should not leak `this.proxy` to setup()', () => {
  885. const source = vi.fn()
  886. const Comp = {
  887. render() {},
  888. setup() {
  889. watch(source, () => {})
  890. }
  891. }
  892. const root = document.createElement('div')
  893. new Vue(Comp).$mount(root)
  894. // should not have any arguments
  895. expect(source.mock.calls[0]).toMatchObject([])
  896. })
  897. // vuejs/core#2728
  898. test('pre watcher callbacks should not track dependencies', async () => {
  899. const a = ref(0)
  900. const b = ref(0)
  901. const updated = vi.fn()
  902. const cb = vi.fn()
  903. const Child = {
  904. props: ['a'],
  905. updated,
  906. watch: {
  907. a() {
  908. cb()
  909. b.value
  910. }
  911. },
  912. render() {
  913. return h('div', this.a)
  914. }
  915. }
  916. const Parent = {
  917. render() {
  918. return h(Child, { props: { a: a.value } })
  919. }
  920. }
  921. const root = document.createElement('div')
  922. new Vue(Parent).$mount(root)
  923. a.value++
  924. await nextTick()
  925. expect(updated).toHaveBeenCalledTimes(1)
  926. expect(cb).toHaveBeenCalledTimes(1)
  927. b.value++
  928. await nextTick()
  929. // should not track b as dependency of Child
  930. expect(updated).toHaveBeenCalledTimes(1)
  931. expect(cb).toHaveBeenCalledTimes(1)
  932. })
  933. it('watching sources: ref<any[]>', async () => {
  934. const foo = ref([1])
  935. const spy = vi.fn()
  936. watch(foo, () => {
  937. spy()
  938. })
  939. foo.value = foo.value.slice()
  940. await nextTick()
  941. expect(spy).toBeCalledTimes(1)
  942. })
  943. it('watching multiple sources: computed', async () => {
  944. let count = 0
  945. const value = ref('1')
  946. const plus = computed(() => !!value.value)
  947. watch([plus], () => {
  948. count++
  949. })
  950. value.value = '2'
  951. await nextTick()
  952. expect(plus.value).toBe(true)
  953. expect(count).toBe(0)
  954. })
  955. // vuejs/core#4158
  956. test('watch should not register in owner component if created inside detached scope', () => {
  957. let instance: Component
  958. const Comp = {
  959. setup() {
  960. instance = getCurrentInstance()!.proxy
  961. effectScope(true).run(() => {
  962. watch(
  963. () => 1,
  964. () => {}
  965. )
  966. })
  967. return () => ''
  968. }
  969. }
  970. const root = document.createElement('div')
  971. new Vue(Comp).$mount(root)
  972. // should not record watcher in detached scope and only the instance's
  973. // own update effect
  974. expect(instance!._scope.effects.length).toBe(1)
  975. })
  976. // #12578
  977. test('template ref triggered watcher should fire after component mount', async () => {
  978. const order: string[] = []
  979. const Child = { template: '<div/>' }
  980. const App = {
  981. setup() {
  982. const child = ref<any>(null)
  983. onMounted(() => {
  984. order.push('mounted')
  985. })
  986. watch(child, () => {
  987. order.push('watcher')
  988. })
  989. return { child }
  990. },
  991. components: { Child },
  992. template: `<Child ref="child"/>`
  993. }
  994. new Vue(App).$mount()
  995. await nextTick()
  996. expect(order).toMatchObject([`mounted`, `watcher`])
  997. })
  998. })