apiWatch.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. import {
  2. watch,
  3. watchEffect,
  4. reactive,
  5. computed,
  6. nextTick,
  7. ref,
  8. h
  9. } from '../src/index'
  10. import { render, nodeOps, serializeInner, TestElement } from '@vue/runtime-test'
  11. import {
  12. ITERATE_KEY,
  13. DebuggerEvent,
  14. TrackOpTypes,
  15. TriggerOpTypes,
  16. triggerRef,
  17. shallowRef
  18. } from '@vue/reactivity'
  19. // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
  20. describe('api: watch', () => {
  21. it('effect', async () => {
  22. const state = reactive({ count: 0 })
  23. let dummy
  24. watchEffect(() => {
  25. dummy = state.count
  26. })
  27. expect(dummy).toBe(0)
  28. state.count++
  29. await nextTick()
  30. expect(dummy).toBe(1)
  31. })
  32. it('watching single source: getter', async () => {
  33. const state = reactive({ count: 0 })
  34. let dummy
  35. watch(
  36. () => state.count,
  37. (count, prevCount) => {
  38. dummy = [count, prevCount]
  39. // assert types
  40. count + 1
  41. if (prevCount) {
  42. prevCount + 1
  43. }
  44. }
  45. )
  46. state.count++
  47. await nextTick()
  48. expect(dummy).toMatchObject([1, 0])
  49. })
  50. it('watching single source: ref', async () => {
  51. const count = ref(0)
  52. let dummy
  53. watch(count, (count, prevCount) => {
  54. dummy = [count, prevCount]
  55. // assert types
  56. count + 1
  57. if (prevCount) {
  58. prevCount + 1
  59. }
  60. })
  61. count.value++
  62. await nextTick()
  63. expect(dummy).toMatchObject([1, 0])
  64. })
  65. it('watching single source: array', async () => {
  66. const array = reactive([] as number[])
  67. const spy = jest.fn()
  68. watch(array, spy)
  69. array.push(1)
  70. await nextTick()
  71. expect(spy).toBeCalledTimes(1)
  72. expect(spy).toBeCalledWith([1], expect.anything(), expect.anything())
  73. })
  74. it('should not fire if watched getter result did not change', async () => {
  75. const spy = jest.fn()
  76. const n = ref(0)
  77. watch(() => n.value % 2, spy)
  78. n.value++
  79. await nextTick()
  80. expect(spy).toBeCalledTimes(1)
  81. n.value += 2
  82. await nextTick()
  83. // should not be called again because getter result did not change
  84. expect(spy).toBeCalledTimes(1)
  85. })
  86. it('watching single source: computed ref', async () => {
  87. const count = ref(0)
  88. const plus = computed(() => count.value + 1)
  89. let dummy
  90. watch(plus, (count, prevCount) => {
  91. dummy = [count, prevCount]
  92. // assert types
  93. count + 1
  94. if (prevCount) {
  95. prevCount + 1
  96. }
  97. })
  98. count.value++
  99. await nextTick()
  100. expect(dummy).toMatchObject([2, 1])
  101. })
  102. it('watching primitive with deep: true', async () => {
  103. const count = ref(0)
  104. let dummy
  105. watch(
  106. count,
  107. (c, prevCount) => {
  108. dummy = [c, prevCount]
  109. },
  110. {
  111. deep: true
  112. }
  113. )
  114. count.value++
  115. await nextTick()
  116. expect(dummy).toMatchObject([1, 0])
  117. })
  118. it('directly watching reactive object (with automatic deep: true)', async () => {
  119. const src = reactive({
  120. count: 0
  121. })
  122. let dummy
  123. watch(src, ({ count }) => {
  124. dummy = count
  125. })
  126. src.count++
  127. await nextTick()
  128. expect(dummy).toBe(1)
  129. })
  130. it('watching multiple sources', async () => {
  131. const state = reactive({ count: 1 })
  132. const count = ref(1)
  133. const plus = computed(() => count.value + 1)
  134. let dummy
  135. watch([() => state.count, count, plus], (vals, oldVals) => {
  136. dummy = [vals, oldVals]
  137. // assert types
  138. vals.concat(1)
  139. oldVals.concat(1)
  140. })
  141. state.count++
  142. count.value++
  143. await nextTick()
  144. expect(dummy).toMatchObject([[2, 2, 3], [1, 1, 2]])
  145. })
  146. it('watching multiple sources: readonly array', async () => {
  147. const state = reactive({ count: 1 })
  148. const status = ref(false)
  149. let dummy
  150. watch([() => state.count, status] as const, (vals, oldVals) => {
  151. dummy = [vals, oldVals]
  152. const [count] = vals
  153. const [, oldStatus] = oldVals
  154. // assert types
  155. count + 1
  156. oldStatus === true
  157. })
  158. state.count++
  159. status.value = true
  160. await nextTick()
  161. expect(dummy).toMatchObject([[2, true], [1, false]])
  162. })
  163. it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
  164. const src = reactive({ count: 0 })
  165. let dummy
  166. watch([src], ([state]) => {
  167. dummy = state
  168. // assert types
  169. state.count === 1
  170. })
  171. src.count++
  172. await nextTick()
  173. expect(dummy).toMatchObject({ count: 1 })
  174. })
  175. it('warn invalid watch source', () => {
  176. // @ts-ignore
  177. watch(1, () => {})
  178. expect(`Invalid watch source`).toHaveBeenWarned()
  179. })
  180. it('warn invalid watch source: multiple sources', () => {
  181. watch([1], () => {})
  182. expect(`Invalid watch source`).toHaveBeenWarned()
  183. })
  184. it('stopping the watcher (effect)', async () => {
  185. const state = reactive({ count: 0 })
  186. let dummy
  187. const stop = watchEffect(() => {
  188. dummy = state.count
  189. })
  190. expect(dummy).toBe(0)
  191. stop()
  192. state.count++
  193. await nextTick()
  194. // should not update
  195. expect(dummy).toBe(0)
  196. })
  197. it('stopping the watcher (with source)', async () => {
  198. const state = reactive({ count: 0 })
  199. let dummy
  200. const stop = watch(
  201. () => state.count,
  202. count => {
  203. dummy = count
  204. }
  205. )
  206. state.count++
  207. await nextTick()
  208. expect(dummy).toBe(1)
  209. stop()
  210. state.count++
  211. await nextTick()
  212. // should not update
  213. expect(dummy).toBe(1)
  214. })
  215. it('cleanup registration (effect)', async () => {
  216. const state = reactive({ count: 0 })
  217. const cleanup = jest.fn()
  218. let dummy
  219. const stop = watchEffect(onCleanup => {
  220. onCleanup(cleanup)
  221. dummy = state.count
  222. })
  223. expect(dummy).toBe(0)
  224. state.count++
  225. await nextTick()
  226. expect(cleanup).toHaveBeenCalledTimes(1)
  227. expect(dummy).toBe(1)
  228. stop()
  229. expect(cleanup).toHaveBeenCalledTimes(2)
  230. })
  231. it('cleanup registration (with source)', async () => {
  232. const count = ref(0)
  233. const cleanup = jest.fn()
  234. let dummy
  235. const stop = watch(count, (count, prevCount, onCleanup) => {
  236. onCleanup(cleanup)
  237. dummy = count
  238. })
  239. count.value++
  240. await nextTick()
  241. expect(cleanup).toHaveBeenCalledTimes(0)
  242. expect(dummy).toBe(1)
  243. count.value++
  244. await nextTick()
  245. expect(cleanup).toHaveBeenCalledTimes(1)
  246. expect(dummy).toBe(2)
  247. stop()
  248. expect(cleanup).toHaveBeenCalledTimes(2)
  249. })
  250. it('flush timing: pre (default)', async () => {
  251. const count = ref(0)
  252. const count2 = ref(0)
  253. let callCount = 0
  254. let result1
  255. let result2
  256. const assertion = jest.fn((count, count2Value) => {
  257. callCount++
  258. // on mount, the watcher callback should be called before DOM render
  259. // on update, should be called before the count is updated
  260. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  261. result1 = serializeInner(root) === expectedDOM
  262. // in a pre-flush callback, all state should have been updated
  263. const expectedState = callCount - 1
  264. result2 = count === expectedState && count2Value === expectedState
  265. })
  266. const Comp = {
  267. setup() {
  268. watchEffect(() => {
  269. assertion(count.value, count2.value)
  270. })
  271. return () => count.value
  272. }
  273. }
  274. const root = nodeOps.createElement('div')
  275. render(h(Comp), root)
  276. expect(assertion).toHaveBeenCalledTimes(1)
  277. expect(result1).toBe(true)
  278. expect(result2).toBe(true)
  279. count.value++
  280. count2.value++
  281. await nextTick()
  282. // two mutations should result in 1 callback execution
  283. expect(assertion).toHaveBeenCalledTimes(2)
  284. expect(result1).toBe(true)
  285. expect(result2).toBe(true)
  286. })
  287. it('flush timing: post', async () => {
  288. const count = ref(0)
  289. let result
  290. const assertion = jest.fn(count => {
  291. result = serializeInner(root) === `${count}`
  292. })
  293. const Comp = {
  294. setup() {
  295. watchEffect(
  296. () => {
  297. assertion(count.value)
  298. },
  299. { flush: 'post' }
  300. )
  301. return () => count.value
  302. }
  303. }
  304. const root = nodeOps.createElement('div')
  305. render(h(Comp), root)
  306. expect(assertion).toHaveBeenCalledTimes(1)
  307. expect(result).toBe(true)
  308. count.value++
  309. await nextTick()
  310. expect(assertion).toHaveBeenCalledTimes(2)
  311. expect(result).toBe(true)
  312. })
  313. it('flush timing: sync', async () => {
  314. const count = ref(0)
  315. const count2 = ref(0)
  316. let callCount = 0
  317. let result1
  318. let result2
  319. const assertion = jest.fn(count => {
  320. callCount++
  321. // on mount, the watcher callback should be called before DOM render
  322. // on update, should be called before the count is updated
  323. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  324. result1 = serializeInner(root) === expectedDOM
  325. // in a sync callback, state mutation on the next line should not have
  326. // executed yet on the 2nd call, but will be on the 3rd call.
  327. const expectedState = callCount < 3 ? 0 : 1
  328. result2 = count2.value === expectedState
  329. })
  330. const Comp = {
  331. setup() {
  332. watchEffect(
  333. () => {
  334. assertion(count.value)
  335. },
  336. {
  337. flush: 'sync'
  338. }
  339. )
  340. return () => count.value
  341. }
  342. }
  343. const root = nodeOps.createElement('div')
  344. render(h(Comp), root)
  345. expect(assertion).toHaveBeenCalledTimes(1)
  346. expect(result1).toBe(true)
  347. expect(result2).toBe(true)
  348. count.value++
  349. count2.value++
  350. await nextTick()
  351. expect(assertion).toHaveBeenCalledTimes(3)
  352. expect(result1).toBe(true)
  353. expect(result2).toBe(true)
  354. })
  355. it('should not fire on component unmount w/ flush: post', async () => {
  356. const toggle = ref(true)
  357. const cb = jest.fn()
  358. const Comp = {
  359. setup() {
  360. watch(toggle, cb, { flush: 'post' })
  361. },
  362. render() {}
  363. }
  364. const App = {
  365. render() {
  366. return toggle.value ? h(Comp) : null
  367. }
  368. }
  369. render(h(App), nodeOps.createElement('div'))
  370. expect(cb).not.toHaveBeenCalled()
  371. toggle.value = false
  372. await nextTick()
  373. expect(cb).not.toHaveBeenCalled()
  374. })
  375. it('should fire on component unmount w/ flush: pre', async () => {
  376. const toggle = ref(true)
  377. const cb = jest.fn()
  378. const Comp = {
  379. setup() {
  380. watch(toggle, cb, { flush: 'pre' })
  381. },
  382. render() {}
  383. }
  384. const App = {
  385. render() {
  386. return toggle.value ? h(Comp) : null
  387. }
  388. }
  389. render(h(App), nodeOps.createElement('div'))
  390. expect(cb).not.toHaveBeenCalled()
  391. toggle.value = false
  392. await nextTick()
  393. expect(cb).toHaveBeenCalledTimes(1)
  394. })
  395. // #1763
  396. it('flush: pre watcher watching props should fire before child update', async () => {
  397. const a = ref(0)
  398. const b = ref(0)
  399. const c = ref(0)
  400. const calls: string[] = []
  401. const Comp = {
  402. props: ['a', 'b'],
  403. setup(props: any) {
  404. watch(
  405. () => props.a + props.b,
  406. () => {
  407. calls.push('watcher 1')
  408. c.value++
  409. },
  410. { flush: 'pre' }
  411. )
  412. // #1777 chained pre-watcher
  413. watch(
  414. c,
  415. () => {
  416. calls.push('watcher 2')
  417. },
  418. { flush: 'pre' }
  419. )
  420. return () => {
  421. c.value
  422. calls.push('render')
  423. }
  424. }
  425. }
  426. const App = {
  427. render() {
  428. return h(Comp, { a: a.value, b: b.value })
  429. }
  430. }
  431. render(h(App), nodeOps.createElement('div'))
  432. expect(calls).toEqual(['render'])
  433. // both props are updated
  434. // should trigger pre-flush watcher first and only once
  435. // then trigger child render
  436. a.value++
  437. b.value++
  438. await nextTick()
  439. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  440. })
  441. // #1852
  442. it('flush: post watcher should fire after template refs updated', async () => {
  443. const toggle = ref(false)
  444. let dom: TestElement | null = null
  445. const App = {
  446. setup() {
  447. const domRef = ref<TestElement | null>(null)
  448. watch(
  449. toggle,
  450. () => {
  451. dom = domRef.value
  452. },
  453. { flush: 'post' }
  454. )
  455. return () => {
  456. return toggle.value ? h('p', { ref: domRef }) : null
  457. }
  458. }
  459. }
  460. render(h(App), nodeOps.createElement('div'))
  461. expect(dom).toBe(null)
  462. toggle.value = true
  463. await nextTick()
  464. expect(dom!.tag).toBe('p')
  465. })
  466. it('deep', async () => {
  467. const state = reactive({
  468. nested: {
  469. count: ref(0)
  470. },
  471. array: [1, 2, 3],
  472. map: new Map([['a', 1], ['b', 2]]),
  473. set: new Set([1, 2, 3])
  474. })
  475. let dummy
  476. watch(
  477. () => state,
  478. state => {
  479. dummy = [
  480. state.nested.count,
  481. state.array[0],
  482. state.map.get('a'),
  483. state.set.has(1)
  484. ]
  485. },
  486. { deep: true }
  487. )
  488. state.nested.count++
  489. await nextTick()
  490. expect(dummy).toEqual([1, 1, 1, true])
  491. // nested array mutation
  492. state.array[0] = 2
  493. await nextTick()
  494. expect(dummy).toEqual([1, 2, 1, true])
  495. // nested map mutation
  496. state.map.set('a', 2)
  497. await nextTick()
  498. expect(dummy).toEqual([1, 2, 2, true])
  499. // nested set mutation
  500. state.set.delete(1)
  501. await nextTick()
  502. expect(dummy).toEqual([1, 2, 2, false])
  503. })
  504. it('watching deep ref', async () => {
  505. const count = ref(0)
  506. const double = computed(() => count.value * 2)
  507. const state = reactive([count, double])
  508. let dummy
  509. watch(
  510. () => state,
  511. state => {
  512. dummy = [state[0].value, state[1].value]
  513. },
  514. { deep: true }
  515. )
  516. count.value++
  517. await nextTick()
  518. expect(dummy).toEqual([1, 2])
  519. })
  520. it('immediate', async () => {
  521. const count = ref(0)
  522. const cb = jest.fn()
  523. watch(count, cb, { immediate: true })
  524. expect(cb).toHaveBeenCalledTimes(1)
  525. count.value++
  526. await nextTick()
  527. expect(cb).toHaveBeenCalledTimes(2)
  528. })
  529. it('immediate: triggers when initial value is null', async () => {
  530. const state = ref(null)
  531. const spy = jest.fn()
  532. watch(() => state.value, spy, { immediate: true })
  533. expect(spy).toHaveBeenCalled()
  534. })
  535. it('immediate: triggers when initial value is undefined', async () => {
  536. const state = ref()
  537. const spy = jest.fn()
  538. watch(() => state.value, spy, { immediate: true })
  539. expect(spy).toHaveBeenCalled()
  540. state.value = 3
  541. await nextTick()
  542. expect(spy).toHaveBeenCalledTimes(2)
  543. // testing if undefined can trigger the watcher
  544. state.value = undefined
  545. await nextTick()
  546. expect(spy).toHaveBeenCalledTimes(3)
  547. // it shouldn't trigger if the same value is set
  548. state.value = undefined
  549. await nextTick()
  550. expect(spy).toHaveBeenCalledTimes(3)
  551. })
  552. it('warn immediate option when using effect', async () => {
  553. const count = ref(0)
  554. let dummy
  555. watchEffect(
  556. () => {
  557. dummy = count.value
  558. },
  559. // @ts-ignore
  560. { immediate: false }
  561. )
  562. expect(dummy).toBe(0)
  563. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  564. count.value++
  565. await nextTick()
  566. expect(dummy).toBe(1)
  567. })
  568. it('warn and not respect deep option when using effect', async () => {
  569. const arr = ref([1, [2]])
  570. const spy = jest.fn()
  571. watchEffect(
  572. () => {
  573. spy()
  574. return arr
  575. },
  576. // @ts-ignore
  577. { deep: true }
  578. )
  579. expect(spy).toHaveBeenCalledTimes(1)
  580. ;(arr.value[1] as Array<number>)[0] = 3
  581. await nextTick()
  582. expect(spy).toHaveBeenCalledTimes(1)
  583. expect(`"deep" option is only respected`).toHaveBeenWarned()
  584. })
  585. it('onTrack', async () => {
  586. const events: DebuggerEvent[] = []
  587. let dummy
  588. const onTrack = jest.fn((e: DebuggerEvent) => {
  589. events.push(e)
  590. })
  591. const obj = reactive({ foo: 1, bar: 2 })
  592. watchEffect(
  593. () => {
  594. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  595. },
  596. { onTrack }
  597. )
  598. await nextTick()
  599. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  600. expect(onTrack).toHaveBeenCalledTimes(3)
  601. expect(events).toMatchObject([
  602. {
  603. target: obj,
  604. type: TrackOpTypes.GET,
  605. key: 'foo'
  606. },
  607. {
  608. target: obj,
  609. type: TrackOpTypes.HAS,
  610. key: 'bar'
  611. },
  612. {
  613. target: obj,
  614. type: TrackOpTypes.ITERATE,
  615. key: ITERATE_KEY
  616. }
  617. ])
  618. })
  619. it('onTrigger', async () => {
  620. const events: DebuggerEvent[] = []
  621. let dummy
  622. const onTrigger = jest.fn((e: DebuggerEvent) => {
  623. events.push(e)
  624. })
  625. const obj = reactive({ foo: 1 })
  626. watchEffect(
  627. () => {
  628. dummy = obj.foo
  629. },
  630. { onTrigger }
  631. )
  632. await nextTick()
  633. expect(dummy).toBe(1)
  634. obj.foo++
  635. await nextTick()
  636. expect(dummy).toBe(2)
  637. expect(onTrigger).toHaveBeenCalledTimes(1)
  638. expect(events[0]).toMatchObject({
  639. type: TriggerOpTypes.SET,
  640. key: 'foo',
  641. oldValue: 1,
  642. newValue: 2
  643. })
  644. // @ts-ignore
  645. delete obj.foo
  646. await nextTick()
  647. expect(dummy).toBeUndefined()
  648. expect(onTrigger).toHaveBeenCalledTimes(2)
  649. expect(events[1]).toMatchObject({
  650. type: TriggerOpTypes.DELETE,
  651. key: 'foo',
  652. oldValue: 2
  653. })
  654. })
  655. it('should work sync', () => {
  656. const v = ref(1)
  657. let calls = 0
  658. watch(
  659. v,
  660. () => {
  661. ++calls
  662. },
  663. {
  664. flush: 'sync'
  665. }
  666. )
  667. expect(calls).toBe(0)
  668. v.value++
  669. expect(calls).toBe(1)
  670. })
  671. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  672. const v = shallowRef({ a: 1 })
  673. let sideEffect = 0
  674. watch(v, obj => {
  675. sideEffect = obj.a
  676. })
  677. v.value = v.value
  678. await nextTick()
  679. // should not trigger
  680. expect(sideEffect).toBe(0)
  681. v.value.a++
  682. await nextTick()
  683. // should not trigger
  684. expect(sideEffect).toBe(0)
  685. triggerRef(v)
  686. await nextTick()
  687. // should trigger now
  688. expect(sideEffect).toBe(2)
  689. })
  690. // #2125
  691. test('watchEffect should not recursively trigger itself', async () => {
  692. const spy = jest.fn()
  693. const price = ref(10)
  694. const history = ref<number[]>([])
  695. watchEffect(() => {
  696. history.value.push(price.value)
  697. spy()
  698. })
  699. await nextTick()
  700. expect(spy).toHaveBeenCalledTimes(1)
  701. })
  702. // #2231
  703. test('computed refs should not trigger watch if value has no change', async () => {
  704. const spy = jest.fn()
  705. const source = ref(0)
  706. const price = computed(() => source.value === 0)
  707. watch(price, spy)
  708. source.value++
  709. await nextTick()
  710. source.value++
  711. await nextTick()
  712. expect(spy).toHaveBeenCalledTimes(1)
  713. })
  714. })