apiWatch.spec.ts 25 KB

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