ref.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import {
  2. type Ref,
  3. effect,
  4. isReactive,
  5. isRef,
  6. reactive,
  7. ref,
  8. toRef,
  9. toRefs,
  10. toValue,
  11. } from '../src/index'
  12. import { computed } from '@vue/runtime-dom'
  13. import { customRef, shallowRef, triggerRef, unref } from '../src/ref'
  14. import {
  15. isReadonly,
  16. isShallow,
  17. readonly,
  18. shallowReactive,
  19. } from '../src/reactive'
  20. describe('reactivity/ref', () => {
  21. it('should hold a value', () => {
  22. const a = ref(1)
  23. expect(a.value).toBe(1)
  24. a.value = 2
  25. expect(a.value).toBe(2)
  26. })
  27. it('should be reactive', () => {
  28. const a = ref(1)
  29. let dummy
  30. const fn = vi.fn(() => {
  31. dummy = a.value
  32. })
  33. effect(fn)
  34. expect(fn).toHaveBeenCalledTimes(1)
  35. expect(dummy).toBe(1)
  36. a.value = 2
  37. expect(fn).toHaveBeenCalledTimes(2)
  38. expect(dummy).toBe(2)
  39. // same value should not trigger
  40. a.value = 2
  41. expect(fn).toHaveBeenCalledTimes(2)
  42. })
  43. it('ref wrapped in reactive should not track internal _value access', () => {
  44. const a = ref(1)
  45. const b = reactive(a)
  46. let dummy
  47. const fn = vi.fn(() => {
  48. dummy = b.value // this will observe both b.value and a.value access
  49. })
  50. effect(fn)
  51. expect(fn).toHaveBeenCalledTimes(1)
  52. expect(dummy).toBe(1)
  53. // mutating a.value should only trigger effect once
  54. a.value = 3
  55. expect(fn).toHaveBeenCalledTimes(2)
  56. expect(dummy).toBe(3)
  57. // mutating b.value should trigger the effect twice. (once for a.value change and once for b.value change)
  58. b.value = 5
  59. expect(fn).toHaveBeenCalledTimes(4)
  60. expect(dummy).toBe(5)
  61. })
  62. it('should make nested properties reactive', () => {
  63. const a = ref({
  64. count: 1,
  65. })
  66. let dummy
  67. effect(() => {
  68. dummy = a.value.count
  69. })
  70. expect(dummy).toBe(1)
  71. a.value.count = 2
  72. expect(dummy).toBe(2)
  73. })
  74. it('should work without initial value', () => {
  75. const a = ref()
  76. let dummy
  77. effect(() => {
  78. dummy = a.value
  79. })
  80. expect(dummy).toBe(undefined)
  81. a.value = 2
  82. expect(dummy).toBe(2)
  83. })
  84. it('should work like a normal property when nested in a reactive object', () => {
  85. const a = ref(1)
  86. const obj = reactive({
  87. a,
  88. b: {
  89. c: a,
  90. },
  91. })
  92. let dummy1: number
  93. let dummy2: number
  94. effect(() => {
  95. dummy1 = obj.a
  96. dummy2 = obj.b.c
  97. })
  98. const assertDummiesEqualTo = (val: number) =>
  99. [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
  100. assertDummiesEqualTo(1)
  101. a.value++
  102. assertDummiesEqualTo(2)
  103. obj.a++
  104. assertDummiesEqualTo(3)
  105. obj.b.c++
  106. assertDummiesEqualTo(4)
  107. })
  108. it('should unwrap nested ref in types', () => {
  109. const a = ref(0)
  110. const b = ref(a)
  111. expect(typeof (b.value + 1)).toBe('number')
  112. })
  113. it('should unwrap nested values in types', () => {
  114. const a = {
  115. b: ref(0),
  116. }
  117. const c = ref(a)
  118. expect(typeof (c.value.b + 1)).toBe('number')
  119. })
  120. it('should NOT unwrap ref types nested inside arrays', () => {
  121. const arr = ref([1, ref(3)]).value
  122. expect(isRef(arr[0])).toBe(false)
  123. expect(isRef(arr[1])).toBe(true)
  124. expect((arr[1] as Ref).value).toBe(3)
  125. })
  126. it('should unwrap ref types as props of arrays', () => {
  127. const arr = [ref(0)]
  128. const symbolKey = Symbol('')
  129. arr['' as any] = ref(1)
  130. arr[symbolKey as any] = ref(2)
  131. const arrRef = ref(arr).value
  132. expect(isRef(arrRef[0])).toBe(true)
  133. expect(isRef(arrRef['' as any])).toBe(false)
  134. expect(isRef(arrRef[symbolKey as any])).toBe(false)
  135. expect(arrRef['' as any]).toBe(1)
  136. expect(arrRef[symbolKey as any]).toBe(2)
  137. })
  138. it('should keep tuple types', () => {
  139. const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
  140. 0,
  141. '1',
  142. { a: 1 },
  143. () => 0,
  144. ref(0),
  145. ]
  146. const tupleRef = ref(tuple)
  147. tupleRef.value[0]++
  148. expect(tupleRef.value[0]).toBe(1)
  149. tupleRef.value[1] += '1'
  150. expect(tupleRef.value[1]).toBe('11')
  151. tupleRef.value[2].a++
  152. expect(tupleRef.value[2].a).toBe(2)
  153. expect(tupleRef.value[3]()).toBe(0)
  154. tupleRef.value[4].value++
  155. expect(tupleRef.value[4].value).toBe(1)
  156. })
  157. it('should keep symbols', () => {
  158. const customSymbol = Symbol()
  159. const obj = {
  160. [Symbol.asyncIterator]: ref(1),
  161. [Symbol.hasInstance]: { a: ref('a') },
  162. [Symbol.isConcatSpreadable]: { b: ref(true) },
  163. [Symbol.iterator]: [ref(1)],
  164. [Symbol.match]: new Set<Ref<number>>(),
  165. [Symbol.matchAll]: new Map<number, Ref<string>>(),
  166. [Symbol.replace]: { arr: [ref('a')] },
  167. [Symbol.search]: { set: new Set<Ref<number>>() },
  168. [Symbol.species]: { map: new Map<number, Ref<string>>() },
  169. [Symbol.split]: new WeakSet<Ref<boolean>>(),
  170. [Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
  171. [Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
  172. [Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
  173. [customSymbol]: { arr: [ref(1)] },
  174. }
  175. const objRef = ref(obj)
  176. const keys: (keyof typeof obj)[] = [
  177. Symbol.asyncIterator,
  178. Symbol.hasInstance,
  179. Symbol.isConcatSpreadable,
  180. Symbol.iterator,
  181. Symbol.match,
  182. Symbol.matchAll,
  183. Symbol.replace,
  184. Symbol.search,
  185. Symbol.species,
  186. Symbol.split,
  187. Symbol.toPrimitive,
  188. Symbol.toStringTag,
  189. Symbol.unscopables,
  190. customSymbol,
  191. ]
  192. keys.forEach(key => {
  193. expect(objRef.value[key]).toStrictEqual(obj[key])
  194. })
  195. })
  196. test('unref', () => {
  197. expect(unref(1)).toBe(1)
  198. expect(unref(ref(1))).toBe(1)
  199. })
  200. test('shallowRef', () => {
  201. const sref = shallowRef({ a: 1 })
  202. expect(isReactive(sref.value)).toBe(false)
  203. let dummy
  204. effect(() => {
  205. dummy = sref.value.a
  206. })
  207. expect(dummy).toBe(1)
  208. sref.value = { a: 2 }
  209. expect(isReactive(sref.value)).toBe(false)
  210. expect(dummy).toBe(2)
  211. })
  212. test('shallowRef force trigger', () => {
  213. const sref = shallowRef({ a: 1 })
  214. let dummy
  215. effect(() => {
  216. dummy = sref.value.a
  217. })
  218. expect(dummy).toBe(1)
  219. sref.value.a = 2
  220. expect(dummy).toBe(1) // should not trigger yet
  221. // force trigger
  222. triggerRef(sref)
  223. expect(dummy).toBe(2)
  224. })
  225. test('shallowRef isShallow', () => {
  226. expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
  227. })
  228. test('isRef', () => {
  229. expect(isRef(ref(1))).toBe(true)
  230. expect(isRef(computed(() => 1))).toBe(true)
  231. expect(isRef(0)).toBe(false)
  232. expect(isRef(1)).toBe(false)
  233. // an object that looks like a ref isn't necessarily a ref
  234. expect(isRef({ value: 0 })).toBe(false)
  235. })
  236. test('toRef', () => {
  237. const a = reactive({
  238. x: 1,
  239. })
  240. const x = toRef(a, 'x')
  241. const b = ref({ y: 1 })
  242. const c = toRef(b)
  243. const d = toRef({ z: 1 })
  244. expect(isRef(d)).toBe(true)
  245. expect(d.value.z).toBe(1)
  246. expect(c).toBe(b)
  247. expect(isRef(x)).toBe(true)
  248. expect(x.value).toBe(1)
  249. // source -> proxy
  250. a.x = 2
  251. expect(x.value).toBe(2)
  252. // proxy -> source
  253. x.value = 3
  254. expect(a.x).toBe(3)
  255. // reactivity
  256. let dummyX
  257. effect(() => {
  258. dummyX = x.value
  259. })
  260. expect(dummyX).toBe(x.value)
  261. // mutating source should trigger effect using the proxy refs
  262. a.x = 4
  263. expect(dummyX).toBe(4)
  264. // should keep ref
  265. const r = { x: ref(1) }
  266. expect(toRef(r, 'x')).toBe(r.x)
  267. })
  268. test('toRef on array', () => {
  269. const a = reactive(['a', 'b'])
  270. const r = toRef(a, 1)
  271. expect(r.value).toBe('b')
  272. r.value = 'c'
  273. expect(r.value).toBe('c')
  274. expect(a[1]).toBe('c')
  275. })
  276. test('toRef default value', () => {
  277. const a: { x: number | undefined } = { x: undefined }
  278. const x = toRef(a, 'x', 1)
  279. expect(x.value).toBe(1)
  280. a.x = 2
  281. expect(x.value).toBe(2)
  282. a.x = undefined
  283. expect(x.value).toBe(1)
  284. })
  285. test('toRef getter', () => {
  286. const x = toRef(() => 1)
  287. expect(x.value).toBe(1)
  288. expect(isRef(x)).toBe(true)
  289. expect(unref(x)).toBe(1)
  290. //@ts-expect-error
  291. expect(() => (x.value = 123)).toThrow()
  292. expect(isReadonly(x)).toBe(true)
  293. })
  294. test('toRefs', () => {
  295. const a = reactive({
  296. x: 1,
  297. y: 2,
  298. })
  299. const { x, y } = toRefs(a)
  300. expect(isRef(x)).toBe(true)
  301. expect(isRef(y)).toBe(true)
  302. expect(x.value).toBe(1)
  303. expect(y.value).toBe(2)
  304. // source -> proxy
  305. a.x = 2
  306. a.y = 3
  307. expect(x.value).toBe(2)
  308. expect(y.value).toBe(3)
  309. // proxy -> source
  310. x.value = 3
  311. y.value = 4
  312. expect(a.x).toBe(3)
  313. expect(a.y).toBe(4)
  314. // reactivity
  315. let dummyX, dummyY
  316. effect(() => {
  317. dummyX = x.value
  318. dummyY = y.value
  319. })
  320. expect(dummyX).toBe(x.value)
  321. expect(dummyY).toBe(y.value)
  322. // mutating source should trigger effect using the proxy refs
  323. a.x = 4
  324. a.y = 5
  325. expect(dummyX).toBe(4)
  326. expect(dummyY).toBe(5)
  327. })
  328. test('toRefs should warn on plain object', () => {
  329. toRefs({})
  330. expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
  331. })
  332. test('toRefs should warn on plain array', () => {
  333. toRefs([])
  334. expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
  335. })
  336. test('toRefs reactive array', () => {
  337. const arr = reactive(['a', 'b', 'c'])
  338. const refs = toRefs(arr)
  339. expect(Array.isArray(refs)).toBe(true)
  340. refs[0].value = '1'
  341. expect(arr[0]).toBe('1')
  342. arr[1] = '2'
  343. expect(refs[1].value).toBe('2')
  344. })
  345. test('customRef', () => {
  346. let value = 1
  347. let _trigger: () => void
  348. const custom = customRef((track, trigger) => ({
  349. get() {
  350. track()
  351. return value
  352. },
  353. set(newValue: number) {
  354. value = newValue
  355. _trigger = trigger
  356. },
  357. }))
  358. expect(isRef(custom)).toBe(true)
  359. let dummy
  360. effect(() => {
  361. dummy = custom.value
  362. })
  363. expect(dummy).toBe(1)
  364. custom.value = 2
  365. // should not trigger yet
  366. expect(dummy).toBe(1)
  367. _trigger!()
  368. expect(dummy).toBe(2)
  369. })
  370. test('should not trigger when setting value to same proxy', () => {
  371. const obj = reactive({ count: 0 })
  372. const a = ref(obj)
  373. const spy1 = vi.fn(() => a.value)
  374. effect(spy1)
  375. a.value = obj
  376. expect(spy1).toBeCalledTimes(1)
  377. const b = shallowRef(obj)
  378. const spy2 = vi.fn(() => b.value)
  379. effect(spy2)
  380. b.value = obj
  381. expect(spy2).toBeCalledTimes(1)
  382. })
  383. test('ref should preserve value shallow/readonly-ness', () => {
  384. const original = {}
  385. const r = reactive(original)
  386. const s = shallowReactive(original)
  387. const rr = readonly(original)
  388. const a = ref(original)
  389. expect(a.value).toBe(r)
  390. a.value = s
  391. expect(a.value).toBe(s)
  392. expect(a.value).not.toBe(r)
  393. a.value = rr
  394. expect(a.value).toBe(rr)
  395. expect(a.value).not.toBe(r)
  396. })
  397. test('should not trigger when setting the same raw object', () => {
  398. const obj = {}
  399. const r = ref(obj)
  400. const spy = vi.fn()
  401. effect(() => spy(r.value))
  402. expect(spy).toHaveBeenCalledTimes(1)
  403. r.value = obj
  404. expect(spy).toHaveBeenCalledTimes(1)
  405. })
  406. test('toValue', () => {
  407. const a = ref(1)
  408. const b = computed(() => a.value + 1)
  409. const c = () => a.value + 2
  410. const d = 4
  411. expect(toValue(a)).toBe(1)
  412. expect(toValue(b)).toBe(2)
  413. expect(toValue(c)).toBe(3)
  414. expect(toValue(d)).toBe(4)
  415. })
  416. test('ref w/ customRef w/ getterRef w/ objectRef should store value cache', () => {
  417. const refValue = ref(1)
  418. // @ts-expect-error private field
  419. expect(refValue._value).toBe(1)
  420. let customRefValueCache = 0
  421. const customRefValue = customRef((track, trigger) => {
  422. return {
  423. get() {
  424. track()
  425. return customRefValueCache
  426. },
  427. set(value: number) {
  428. customRefValueCache = value
  429. trigger()
  430. },
  431. }
  432. })
  433. customRefValue.value
  434. // @ts-expect-error internal field
  435. expect(customRefValue._value).toBe(0)
  436. const getterRefValue = toRef(() => 1)
  437. getterRefValue.value
  438. // @ts-expect-error internal field
  439. expect(getterRefValue._value).toBe(1)
  440. const objectRefValue = toRef({ value: 1 }, 'value')
  441. objectRefValue.value
  442. // @ts-expect-error internal field
  443. expect(objectRefValue._value).toBe(1)
  444. })
  445. })