apiWatch.spec.ts 25 KB

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