apiWatch.spec.ts 32 KB

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