apiWatch.spec.ts 24 KB

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