apiWatch.spec.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281
  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], [1], 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. const options = [
  1075. { name: 'only trigger once watch' },
  1076. {
  1077. deep: true,
  1078. name: 'only trigger once watch with deep'
  1079. },
  1080. {
  1081. flush: 'sync',
  1082. name: 'only trigger once watch with flush: sync'
  1083. },
  1084. {
  1085. flush: 'pre',
  1086. name: 'only trigger once watch with flush: pre'
  1087. },
  1088. {
  1089. immediate: true,
  1090. name: 'only trigger once watch with immediate'
  1091. }
  1092. ] as const
  1093. test.each(options)('$name', async option => {
  1094. const count = ref(0)
  1095. const cb = vi.fn()
  1096. watch(count, cb, { once: true, ...option })
  1097. count.value++
  1098. await nextTick()
  1099. expect(count.value).toBe(1)
  1100. expect(cb).toHaveBeenCalledTimes(1)
  1101. count.value++
  1102. await nextTick()
  1103. expect(count.value).toBe(2)
  1104. expect(cb).toHaveBeenCalledTimes(1)
  1105. })
  1106. // #5151
  1107. test('OnCleanup also needs to be cleaned,', async () => {
  1108. const spy1 = vi.fn()
  1109. const spy2 = vi.fn()
  1110. const num = ref(0)
  1111. watch(num, (value, oldValue, onCleanup) => {
  1112. if (value > 1) {
  1113. return
  1114. }
  1115. spy1()
  1116. onCleanup(() => {
  1117. // OnCleanup also needs to be cleaned
  1118. spy2()
  1119. })
  1120. })
  1121. num.value++
  1122. await nextTick()
  1123. expect(spy1).toHaveBeenCalledTimes(1)
  1124. expect(spy2).toHaveBeenCalledTimes(0)
  1125. num.value++
  1126. await nextTick()
  1127. expect(spy1).toHaveBeenCalledTimes(1)
  1128. expect(spy2).toHaveBeenCalledTimes(1)
  1129. num.value++
  1130. await nextTick()
  1131. // would not be calld when value>1
  1132. expect(spy1).toHaveBeenCalledTimes(1)
  1133. expect(spy2).toHaveBeenCalledTimes(1)
  1134. })
  1135. })