apiWatch.spec.ts 25 KB

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