apiWatch.spec.ts 23 KB

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