2
0

apiWatch.spec.ts 17 KB

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