apiWatch.spec.ts 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127
  1. import Vue from 'vue'
  2. import {
  3. watch,
  4. watchEffect,
  5. watchPostEffect,
  6. watchSyncEffect,
  7. reactive,
  8. computed,
  9. ref,
  10. // DebuggerEvent,
  11. // TrackOpTypes,
  12. // TriggerOpTypes,
  13. triggerRef,
  14. shallowRef,
  15. Ref,
  16. h,
  17. getCurrentInstance,
  18. onMounted
  19. // effectScope
  20. } from 'v3'
  21. import { nextTick } from 'core/util'
  22. import { Component } from 'typescript/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([] as number[])
  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 = callCount === 1 ? `` : `${count - 1}`
  271. result1 = root.innerHTML === expectedDOM
  272. // in a pre-flush callback, all state should have been updated
  273. const expectedState = callCount - 1
  274. result2 = count === expectedState && count2Value === expectedState
  275. })
  276. const Comp = {
  277. setup() {
  278. watchEffect(() => {
  279. assertion(count.value, count2.value)
  280. })
  281. return () => count.value
  282. }
  283. }
  284. const root = document.createElement('div')
  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 = root.innerHTML === `${count}`
  302. })
  303. const Comp = {
  304. setup() {
  305. watchEffect(
  306. () => {
  307. assertion(count.value)
  308. },
  309. { flush: 'post' }
  310. )
  311. return () => count.value
  312. }
  313. }
  314. const root = document.createElement('div')
  315. new Vue(Comp).$mount(root)
  316. expect(assertion).toHaveBeenCalledTimes(1)
  317. expect(result).toBe(true)
  318. count.value++
  319. await nextTick()
  320. expect(assertion).toHaveBeenCalledTimes(2)
  321. expect(result).toBe(true)
  322. })
  323. it('watchPostEffect', async () => {
  324. const count = ref(0)
  325. let result
  326. const assertion = vi.fn(count => {
  327. result = root.innerHTML === `${count}`
  328. })
  329. const Comp = {
  330. setup() {
  331. watchPostEffect(() => {
  332. assertion(count.value)
  333. })
  334. return () => count.value
  335. }
  336. }
  337. const root = document.createElement('div')
  338. new Vue(Comp).$mount(root)
  339. expect(assertion).toHaveBeenCalledTimes(1)
  340. expect(result).toBe(true)
  341. count.value++
  342. await nextTick()
  343. expect(assertion).toHaveBeenCalledTimes(2)
  344. expect(result).toBe(true)
  345. })
  346. it('flush timing: sync', async () => {
  347. const count = ref(0)
  348. const count2 = ref(0)
  349. let callCount = 0
  350. let result1
  351. let result2
  352. const assertion = vi.fn(count => {
  353. callCount++
  354. // on mount, the watcher callback should be called before DOM render
  355. // on update, should be called before the count is updated
  356. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  357. result1 = root.innerHTML === expectedDOM
  358. // in a sync callback, state mutation on the next line should not have
  359. // executed yet on the 2nd call, but will be on the 3rd call.
  360. const expectedState = callCount < 3 ? 0 : 1
  361. result2 = count2.value === expectedState
  362. })
  363. const Comp = {
  364. setup() {
  365. watchEffect(
  366. () => {
  367. assertion(count.value)
  368. },
  369. {
  370. flush: 'sync'
  371. }
  372. )
  373. return () => count.value
  374. }
  375. }
  376. const root = document.createElement('div')
  377. new Vue(Comp).$mount(root)
  378. expect(assertion).toHaveBeenCalledTimes(1)
  379. expect(result1).toBe(true)
  380. expect(result2).toBe(true)
  381. count.value++
  382. count2.value++
  383. await nextTick()
  384. expect(assertion).toHaveBeenCalledTimes(3)
  385. expect(result1).toBe(true)
  386. expect(result2).toBe(true)
  387. })
  388. it('watchSyncEffect', async () => {
  389. const count = ref(0)
  390. const count2 = ref(0)
  391. let callCount = 0
  392. let result1
  393. let result2
  394. const assertion = vi.fn(count => {
  395. callCount++
  396. // on mount, the watcher callback should be called before DOM render
  397. // on update, should be called before the count is updated
  398. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  399. result1 = root.innerHTML === expectedDOM
  400. // in a sync callback, state mutation on the next line should not have
  401. // executed yet on the 2nd call, but will be on the 3rd call.
  402. const expectedState = callCount < 3 ? 0 : 1
  403. result2 = count2.value === expectedState
  404. })
  405. const Comp = {
  406. setup() {
  407. watchSyncEffect(() => {
  408. assertion(count.value)
  409. })
  410. return () => count.value
  411. }
  412. }
  413. const root = document.createElement('div')
  414. new Vue(Comp).$mount(root)
  415. expect(assertion).toHaveBeenCalledTimes(1)
  416. expect(result1).toBe(true)
  417. expect(result2).toBe(true)
  418. count.value++
  419. count2.value++
  420. await nextTick()
  421. expect(assertion).toHaveBeenCalledTimes(3)
  422. expect(result1).toBe(true)
  423. expect(result2).toBe(true)
  424. })
  425. it('should not fire on component unmount w/ flush: post', async () => {
  426. const toggle = ref(true)
  427. const cb = vi.fn()
  428. const Comp = {
  429. setup() {
  430. watch(toggle, cb, { flush: 'post' })
  431. },
  432. render() {}
  433. }
  434. const App = {
  435. render() {
  436. return toggle.value ? h(Comp) : null
  437. }
  438. }
  439. new Vue(App).$mount(document.createElement('div'))
  440. expect(cb).not.toHaveBeenCalled()
  441. toggle.value = false
  442. await nextTick()
  443. expect(cb).not.toHaveBeenCalled()
  444. })
  445. it('should fire on component unmount w/ flush: pre', async () => {
  446. const toggle = ref(true)
  447. const cb = vi.fn()
  448. const Comp = {
  449. setup() {
  450. watch(toggle, cb, { flush: 'pre' })
  451. },
  452. render() {}
  453. }
  454. const App = {
  455. render() {
  456. return toggle.value ? h(Comp) : null
  457. }
  458. }
  459. new Vue(App).$mount(document.createElement('div'))
  460. expect(cb).not.toHaveBeenCalled()
  461. toggle.value = false
  462. await nextTick()
  463. expect(cb).toHaveBeenCalledTimes(1)
  464. })
  465. // #1763
  466. it('flush: pre watcher watching props should fire before child update', async () => {
  467. const a = ref(0)
  468. const b = ref(0)
  469. const c = ref(0)
  470. const calls: string[] = []
  471. const Comp = {
  472. props: ['a', 'b'],
  473. setup(props: any) {
  474. watch(
  475. () => props.a + props.b,
  476. () => {
  477. calls.push('watcher 1')
  478. c.value++
  479. },
  480. { flush: 'pre' }
  481. )
  482. // #1777 chained pre-watcher
  483. watch(
  484. c,
  485. () => {
  486. calls.push('watcher 2')
  487. },
  488. { flush: 'pre' }
  489. )
  490. return () => {
  491. c.value
  492. calls.push('render')
  493. }
  494. }
  495. }
  496. const App = {
  497. render() {
  498. return h(Comp, { props: { a: a.value, b: b.value } })
  499. }
  500. }
  501. new Vue(App).$mount(document.createElement('div'))
  502. expect(calls).toEqual(['render'])
  503. // both props are updated
  504. // should trigger pre-flush watcher first and only once
  505. // then trigger child render
  506. a.value++
  507. b.value++
  508. await nextTick()
  509. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  510. })
  511. // #5721
  512. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  513. const count = ref(0)
  514. const calls: string[] = []
  515. const App = {
  516. render() {},
  517. setup() {
  518. watch(
  519. count,
  520. () => {
  521. calls.push('watch ' + count.value)
  522. },
  523. { flush: 'pre' }
  524. )
  525. onMounted(() => {
  526. calls.push('mounted')
  527. })
  528. // mutate multiple times
  529. count.value++
  530. count.value++
  531. count.value++
  532. }
  533. }
  534. new Vue(App).$mount(document.createElement('div'))
  535. expect(calls).toMatchObject(['watch 3', 'mounted'])
  536. })
  537. // #1852
  538. it('flush: post watcher should fire after template refs updated', async () => {
  539. const toggle = ref(false)
  540. let dom: HTMLElement | null = null
  541. const App = {
  542. setup() {
  543. const domRef = ref<any>(null)
  544. watch(
  545. toggle,
  546. () => {
  547. dom = domRef.value
  548. },
  549. { flush: 'post' }
  550. )
  551. return () => {
  552. return toggle.value ? h('p', { ref: domRef }) : null
  553. }
  554. }
  555. }
  556. new Vue(App).$mount(document.createElement('div'))
  557. expect(dom).toBe(null)
  558. toggle.value = true
  559. await nextTick()
  560. expect(dom!.tagName).toBe('P')
  561. })
  562. it('deep', async () => {
  563. const state = reactive({
  564. nested: {
  565. count: ref(0)
  566. },
  567. array: [1, 2, 3],
  568. map: new Map([
  569. ['a', 1],
  570. ['b', 2]
  571. ]),
  572. set: new Set([1, 2, 3])
  573. })
  574. let dummy
  575. watch(
  576. () => state,
  577. state => {
  578. dummy = [
  579. state.nested.count,
  580. state.array[0],
  581. state.map.get('a'),
  582. state.set.has(1)
  583. ]
  584. },
  585. { deep: true }
  586. )
  587. state.nested.count++
  588. await nextTick()
  589. expect(dummy).toEqual([1, 1, 1, true])
  590. // nested array mutation
  591. state.array[0] = 2
  592. await nextTick()
  593. expect(dummy).toEqual([1, 2, 1, true])
  594. // nested map mutation
  595. state.map.set('a', 2)
  596. await nextTick()
  597. expect(dummy).toEqual([1, 2, 2, true])
  598. // nested set mutation
  599. state.set.delete(1)
  600. await nextTick()
  601. expect(dummy).toEqual([1, 2, 2, false])
  602. })
  603. it('watching deep ref', async () => {
  604. const count = ref(0)
  605. const double = computed(() => count.value * 2)
  606. const state = reactive([count, double])
  607. let dummy
  608. watch(
  609. () => state,
  610. state => {
  611. dummy = [state[0].value, state[1].value]
  612. },
  613. { deep: true }
  614. )
  615. count.value++
  616. await nextTick()
  617. expect(dummy).toEqual([1, 2])
  618. })
  619. it('immediate', async () => {
  620. const count = ref(0)
  621. const cb = vi.fn()
  622. watch(count, cb, { immediate: true })
  623. expect(cb).toHaveBeenCalledTimes(1)
  624. count.value++
  625. await nextTick()
  626. expect(cb).toHaveBeenCalledTimes(2)
  627. })
  628. it('immediate: triggers when initial value is null', async () => {
  629. const state = ref(null)
  630. const spy = vi.fn()
  631. watch(() => state.value, spy, { immediate: true })
  632. expect(spy).toHaveBeenCalled()
  633. })
  634. it('immediate: triggers when initial value is undefined', async () => {
  635. const state = ref()
  636. const spy = vi.fn()
  637. watch(() => state.value, spy, { immediate: true })
  638. expect(spy).toHaveBeenCalled()
  639. state.value = 3
  640. await nextTick()
  641. expect(spy).toHaveBeenCalledTimes(2)
  642. // testing if undefined can trigger the watcher
  643. state.value = undefined
  644. await nextTick()
  645. expect(spy).toHaveBeenCalledTimes(3)
  646. // it shouldn't trigger if the same value is set
  647. state.value = undefined
  648. await nextTick()
  649. expect(spy).toHaveBeenCalledTimes(3)
  650. })
  651. it('warn immediate option when using effect', async () => {
  652. const count = ref(0)
  653. let dummy
  654. watchEffect(
  655. () => {
  656. dummy = count.value
  657. },
  658. // @ts-expect-error
  659. { immediate: false }
  660. )
  661. expect(dummy).toBe(0)
  662. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  663. count.value++
  664. await nextTick()
  665. expect(dummy).toBe(1)
  666. })
  667. it('warn and not respect deep option when using effect', async () => {
  668. const arr = ref([1, [2]])
  669. const spy = vi.fn()
  670. watchEffect(
  671. () => {
  672. spy()
  673. return arr
  674. },
  675. // @ts-expect-error
  676. { deep: true }
  677. )
  678. expect(spy).toHaveBeenCalledTimes(1)
  679. ;(arr.value[1] as Array<number>)[0] = 3
  680. await nextTick()
  681. expect(spy).toHaveBeenCalledTimes(1)
  682. expect(`"deep" option is only respected`).toHaveBeenWarned()
  683. })
  684. // TODO
  685. // it('onTrack', async () => {
  686. // const events: DebuggerEvent[] = []
  687. // let dummy
  688. // const onTrack = vi.fn((e: DebuggerEvent) => {
  689. // events.push(e)
  690. // })
  691. // const obj = reactive({ foo: 1, bar: 2 })
  692. // watchEffect(
  693. // () => {
  694. // dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  695. // },
  696. // { onTrack }
  697. // )
  698. // await nextTick()
  699. // expect(dummy).toEqual([1, true, ['foo', 'bar']])
  700. // expect(onTrack).toHaveBeenCalledTimes(3)
  701. // expect(events).toMatchObject([
  702. // {
  703. // target: obj,
  704. // type: TrackOpTypes.GET,
  705. // key: 'foo'
  706. // },
  707. // {
  708. // target: obj,
  709. // type: TrackOpTypes.HAS,
  710. // key: 'bar'
  711. // },
  712. // {
  713. // target: obj,
  714. // type: TrackOpTypes.ITERATE,
  715. // key: ITERATE_KEY
  716. // }
  717. // ])
  718. // })
  719. // it('onTrigger', async () => {
  720. // const events: DebuggerEvent[] = []
  721. // let dummy
  722. // const onTrigger = vi.fn((e: DebuggerEvent) => {
  723. // events.push(e)
  724. // })
  725. // const obj = reactive<{ foo?: number }>({ foo: 1 })
  726. // watchEffect(
  727. // () => {
  728. // dummy = obj.foo
  729. // },
  730. // { onTrigger }
  731. // )
  732. // await nextTick()
  733. // expect(dummy).toBe(1)
  734. // obj.foo!++
  735. // await nextTick()
  736. // expect(dummy).toBe(2)
  737. // expect(onTrigger).toHaveBeenCalledTimes(1)
  738. // expect(events[0]).toMatchObject({
  739. // type: TriggerOpTypes.SET,
  740. // key: 'foo',
  741. // oldValue: 1,
  742. // newValue: 2
  743. // })
  744. // delete obj.foo
  745. // await nextTick()
  746. // expect(dummy).toBeUndefined()
  747. // expect(onTrigger).toHaveBeenCalledTimes(2)
  748. // expect(events[1]).toMatchObject({
  749. // type: TriggerOpTypes.DELETE,
  750. // key: 'foo',
  751. // oldValue: 2
  752. // })
  753. // })
  754. it('should work sync', () => {
  755. const v = ref(1)
  756. let calls = 0
  757. watch(
  758. v,
  759. () => {
  760. ++calls
  761. },
  762. {
  763. flush: 'sync'
  764. }
  765. )
  766. expect(calls).toBe(0)
  767. v.value++
  768. expect(calls).toBe(1)
  769. })
  770. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  771. const v = shallowRef({ a: 1 })
  772. let sideEffect = 0
  773. watch(v, obj => {
  774. sideEffect = obj.a
  775. })
  776. v.value = v.value
  777. await nextTick()
  778. // should not trigger
  779. expect(sideEffect).toBe(0)
  780. v.value.a++
  781. await nextTick()
  782. // should not trigger
  783. expect(sideEffect).toBe(0)
  784. triggerRef(v)
  785. await nextTick()
  786. // should trigger now
  787. expect(sideEffect).toBe(2)
  788. })
  789. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  790. const v = shallowRef([] as any)
  791. const spy = vi.fn()
  792. watch([v], () => {
  793. spy()
  794. })
  795. v.value.push(1)
  796. triggerRef(v)
  797. await nextTick()
  798. // should trigger now
  799. expect(spy).toHaveBeenCalledTimes(1)
  800. })
  801. // #2125
  802. test('watchEffect should not recursively trigger itself', async () => {
  803. const spy = vi.fn()
  804. const price = ref(10)
  805. const history = ref<number[]>([])
  806. watchEffect(() => {
  807. history.value.push(price.value)
  808. spy()
  809. })
  810. await nextTick()
  811. expect(spy).toHaveBeenCalledTimes(1)
  812. })
  813. // #2231
  814. test('computed refs should not trigger watch if value has no change', async () => {
  815. const spy = vi.fn()
  816. const source = ref(0)
  817. const price = computed(() => source.value === 0)
  818. watch(price, spy)
  819. source.value++
  820. await nextTick()
  821. source.value++
  822. await nextTick()
  823. expect(spy).toHaveBeenCalledTimes(1)
  824. })
  825. // TODO
  826. // https://github.com/vuejs/core/issues/2381
  827. test('$watch should always register its effects with its own instance', async () => {
  828. let instance: Component | null
  829. let _show: Ref<boolean>
  830. const Child = {
  831. render: () => h('div'),
  832. mounted() {
  833. instance = getCurrentInstance()!.proxy
  834. },
  835. unmounted() {}
  836. }
  837. const Comp = {
  838. setup() {
  839. const comp = ref<Component | undefined>()
  840. const show = ref(true)
  841. _show = show
  842. return { comp, show }
  843. },
  844. render() {
  845. return this.show
  846. ? h(Child, {
  847. ref: vm => void (this.comp = vm as Component)
  848. })
  849. : null
  850. },
  851. mounted() {
  852. // this call runs while Comp is currentInstance, but
  853. // the effect for this `$watch` should nontheless be registered with Child
  854. this.comp!.$watch(
  855. () => this.show,
  856. () => void 0
  857. )
  858. }
  859. }
  860. new Vue(Comp).$mount(document.createElement('div'))
  861. expect(instance!).toBeDefined()
  862. expect(instance!.scope.effects).toBeInstanceOf(Array)
  863. // includes the component's own render effect AND the watcher effect
  864. expect(instance!.scope.effects.length).toBe(2)
  865. _show!.value = false
  866. await nextTick()
  867. await nextTick()
  868. expect(instance!.scope.effects[0].active).toBe(false)
  869. })
  870. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  871. let instance: any
  872. const source = vi.fn()
  873. const Comp = {
  874. render() {},
  875. created(this: any) {
  876. instance = this
  877. this.$watch(source, function () {})
  878. }
  879. }
  880. const root = document.createElement('div')
  881. new Vue(Comp).$mount(root)
  882. expect(instance).toBeDefined()
  883. expect(source).toHaveBeenCalledWith(instance)
  884. })
  885. test('should not leak `this.proxy` to setup()', () => {
  886. const source = vi.fn()
  887. const Comp = {
  888. render() {},
  889. setup() {
  890. watch(source, () => {})
  891. }
  892. }
  893. const root = document.createElement('div')
  894. new Vue(Comp).$mount(root)
  895. // should not have any arguments
  896. expect(source.mock.calls[0]).toMatchObject([])
  897. })
  898. // #2728
  899. test('pre watcher callbacks should not track dependencies', async () => {
  900. const a = ref(0)
  901. const b = ref(0)
  902. const updated = vi.fn()
  903. const Child = {
  904. props: ['a'],
  905. updated,
  906. watch: {
  907. a() {
  908. b.value
  909. }
  910. },
  911. render() {
  912. return h('div', this.a)
  913. }
  914. }
  915. const Parent = {
  916. render() {
  917. return h(Child, { a: a.value })
  918. }
  919. }
  920. const root = document.createElement('div')
  921. new Vue(Parent).$mount(root)
  922. a.value++
  923. await nextTick()
  924. expect(updated).toHaveBeenCalledTimes(1)
  925. b.value++
  926. await nextTick()
  927. // should not track b as dependency of Child
  928. expect(updated).toHaveBeenCalledTimes(1)
  929. })
  930. test('watching keypath', async () => {
  931. const spy = vi.fn()
  932. const Comp = {
  933. render() {},
  934. data() {
  935. return {
  936. a: {
  937. b: 1
  938. }
  939. }
  940. },
  941. watch: {
  942. 'a.b': spy
  943. },
  944. created(this: any) {
  945. this.$watch('a.b', spy)
  946. },
  947. mounted(this: any) {
  948. this.a.b++
  949. }
  950. }
  951. const root = document.createElement('div')
  952. new Vue(Comp).$mount(root)
  953. await nextTick()
  954. expect(spy).toHaveBeenCalledTimes(2)
  955. })
  956. it('watching sources: ref<any[]>', async () => {
  957. const foo = ref([1])
  958. const spy = vi.fn()
  959. watch(foo, () => {
  960. spy()
  961. })
  962. foo.value = foo.value.slice()
  963. await nextTick()
  964. expect(spy).toBeCalledTimes(1)
  965. })
  966. it('watching multiple sources: computed', async () => {
  967. let count = 0
  968. const value = ref('1')
  969. const plus = computed(() => !!value.value)
  970. watch([plus], () => {
  971. count++
  972. })
  973. value.value = '2'
  974. await nextTick()
  975. expect(plus.value).toBe(true)
  976. expect(count).toBe(0)
  977. })
  978. // #4158
  979. // TODO
  980. test.skip('watch should not register in owner component if created inside detached scope', () => {
  981. let instance: Component
  982. const Comp = {
  983. setup() {
  984. instance = getCurrentInstance()!.proxy
  985. effectScope(true).run(() => {
  986. watch(
  987. () => 1,
  988. () => {}
  989. )
  990. })
  991. return () => ''
  992. }
  993. }
  994. const root = document.createElement('div')
  995. new Vue(Comp).$mount(root)
  996. // should not record watcher in detached scope and only the instance's
  997. // own update effect
  998. expect(instance!.scope.effects.length).toBe(1)
  999. })
  1000. })