apiWatch.spec.ts 28 KB

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