apiWatch.spec.ts 28 KB

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