reactive.spec.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { ref, isRef } from '../src/ref'
  2. import { reactive, isReactive, toRaw, markRaw } from '../src/reactive'
  3. import { computed } from '../src/computed'
  4. import { effect } from '../src/effect'
  5. describe('reactivity/reactive', () => {
  6. test('Object', () => {
  7. const original = { foo: 1 }
  8. const observed = reactive(original)
  9. expect(observed).not.toBe(original)
  10. expect(isReactive(observed)).toBe(true)
  11. expect(isReactive(original)).toBe(false)
  12. // get
  13. expect(observed.foo).toBe(1)
  14. // has
  15. expect('foo' in observed).toBe(true)
  16. // ownKeys
  17. expect(Object.keys(observed)).toEqual(['foo'])
  18. })
  19. test('proto', () => {
  20. const obj = {}
  21. const reactiveObj = reactive(obj)
  22. expect(isReactive(reactiveObj)).toBe(true)
  23. // read prop of reactiveObject will cause reactiveObj[prop] to be reactive
  24. // @ts-ignore
  25. const prototype = reactiveObj['__proto__']
  26. const otherObj = { data: ['a'] }
  27. expect(isReactive(otherObj)).toBe(false)
  28. const reactiveOther = reactive(otherObj)
  29. expect(isReactive(reactiveOther)).toBe(true)
  30. expect(reactiveOther.data[0]).toBe('a')
  31. })
  32. test('nested reactives', () => {
  33. const original = {
  34. nested: {
  35. foo: 1
  36. },
  37. array: [{ bar: 2 }]
  38. }
  39. const observed = reactive(original)
  40. expect(isReactive(observed.nested)).toBe(true)
  41. expect(isReactive(observed.array)).toBe(true)
  42. expect(isReactive(observed.array[0])).toBe(true)
  43. })
  44. test('observing subtypes of IterableCollections(Map, Set)', () => {
  45. // subtypes of Map
  46. class CustomMap extends Map {}
  47. const cmap = reactive(new CustomMap())
  48. expect(cmap instanceof Map).toBe(true)
  49. expect(isReactive(cmap)).toBe(true)
  50. cmap.set('key', {})
  51. expect(isReactive(cmap.get('key'))).toBe(true)
  52. // subtypes of Set
  53. class CustomSet extends Set {}
  54. const cset = reactive(new CustomSet())
  55. expect(cset instanceof Set).toBe(true)
  56. expect(isReactive(cset)).toBe(true)
  57. let dummy
  58. effect(() => (dummy = cset.has('value')))
  59. expect(dummy).toBe(false)
  60. cset.add('value')
  61. expect(dummy).toBe(true)
  62. cset.delete('value')
  63. expect(dummy).toBe(false)
  64. })
  65. test('observing subtypes of WeakCollections(WeakMap, WeakSet)', () => {
  66. // subtypes of WeakMap
  67. class CustomMap extends WeakMap {}
  68. const cmap = reactive(new CustomMap())
  69. expect(cmap instanceof WeakMap).toBe(true)
  70. expect(isReactive(cmap)).toBe(true)
  71. const key = {}
  72. cmap.set(key, {})
  73. expect(isReactive(cmap.get(key))).toBe(true)
  74. // subtypes of WeakSet
  75. class CustomSet extends WeakSet {}
  76. const cset = reactive(new CustomSet())
  77. expect(cset instanceof WeakSet).toBe(true)
  78. expect(isReactive(cset)).toBe(true)
  79. let dummy
  80. effect(() => (dummy = cset.has(key)))
  81. expect(dummy).toBe(false)
  82. cset.add(key)
  83. expect(dummy).toBe(true)
  84. cset.delete(key)
  85. expect(dummy).toBe(false)
  86. })
  87. test('observed value should proxy mutations to original (Object)', () => {
  88. const original: any = { foo: 1 }
  89. const observed = reactive(original)
  90. // set
  91. observed.bar = 1
  92. expect(observed.bar).toBe(1)
  93. expect(original.bar).toBe(1)
  94. // delete
  95. delete observed.foo
  96. expect('foo' in observed).toBe(false)
  97. expect('foo' in original).toBe(false)
  98. })
  99. test('original value change should reflect in observed value (Object)', () => {
  100. const original: any = { foo: 1 }
  101. const observed = reactive(original)
  102. // set
  103. original.bar = 1
  104. expect(original.bar).toBe(1)
  105. expect(observed.bar).toBe(1)
  106. // delete
  107. delete original.foo
  108. expect('foo' in original).toBe(false)
  109. expect('foo' in observed).toBe(false)
  110. })
  111. test('setting a property with an unobserved value should wrap with reactive', () => {
  112. const observed = reactive<{ foo?: object }>({})
  113. const raw = {}
  114. observed.foo = raw
  115. expect(observed.foo).not.toBe(raw)
  116. expect(isReactive(observed.foo)).toBe(true)
  117. })
  118. test('observing already observed value should return same Proxy', () => {
  119. const original = { foo: 1 }
  120. const observed = reactive(original)
  121. const observed2 = reactive(observed)
  122. expect(observed2).toBe(observed)
  123. })
  124. test('observing the same value multiple times should return same Proxy', () => {
  125. const original = { foo: 1 }
  126. const observed = reactive(original)
  127. const observed2 = reactive(original)
  128. expect(observed2).toBe(observed)
  129. })
  130. test('should not pollute original object with Proxies', () => {
  131. const original: any = { foo: 1 }
  132. const original2 = { bar: 2 }
  133. const observed = reactive(original)
  134. const observed2 = reactive(original2)
  135. observed.bar = observed2
  136. expect(observed.bar).toBe(observed2)
  137. expect(original.bar).toBe(original2)
  138. })
  139. test('toRaw', () => {
  140. const original = { foo: 1 }
  141. const observed = reactive(original)
  142. expect(toRaw(observed)).toBe(original)
  143. expect(toRaw(original)).toBe(original)
  144. })
  145. test('toRaw on object using reactive as prototype', () => {
  146. const original = reactive({})
  147. const obj = Object.create(original)
  148. const raw = toRaw(obj)
  149. expect(raw).toBe(obj)
  150. expect(raw).not.toBe(toRaw(original))
  151. })
  152. test('should not unwrap Ref<T>', () => {
  153. const observedNumberRef = reactive(ref(1))
  154. const observedObjectRef = reactive(ref({ foo: 1 }))
  155. expect(isRef(observedNumberRef)).toBe(true)
  156. expect(isRef(observedObjectRef)).toBe(true)
  157. })
  158. test('should unwrap computed refs', () => {
  159. // readonly
  160. const a = computed(() => 1)
  161. // writable
  162. const b = computed({
  163. get: () => 1,
  164. set: () => {}
  165. })
  166. const obj = reactive({ a, b })
  167. // check type
  168. obj.a + 1
  169. obj.b + 1
  170. expect(typeof obj.a).toBe(`number`)
  171. expect(typeof obj.b).toBe(`number`)
  172. })
  173. test('should allow setting property from a ref to another ref', () => {
  174. const foo = ref(0)
  175. const bar = ref(1)
  176. const observed = reactive({ a: foo })
  177. const dummy = computed(() => observed.a)
  178. expect(dummy.value).toBe(0)
  179. // @ts-ignore
  180. observed.a = bar
  181. expect(dummy.value).toBe(1)
  182. bar.value++
  183. expect(dummy.value).toBe(2)
  184. })
  185. test('non-observable values', () => {
  186. const assertValue = (value: any) => {
  187. reactive(value)
  188. expect(
  189. `value cannot be made reactive: ${String(value)}`
  190. ).toHaveBeenWarnedLast()
  191. }
  192. // number
  193. assertValue(1)
  194. // string
  195. assertValue('foo')
  196. // boolean
  197. assertValue(false)
  198. // null
  199. assertValue(null)
  200. // undefined
  201. assertValue(undefined)
  202. // symbol
  203. const s = Symbol()
  204. assertValue(s)
  205. // built-ins should work and return same value
  206. const p = Promise.resolve()
  207. expect(reactive(p)).toBe(p)
  208. const r = new RegExp('')
  209. expect(reactive(r)).toBe(r)
  210. const d = new Date()
  211. expect(reactive(d)).toBe(d)
  212. })
  213. test('markRaw', () => {
  214. const obj = reactive({
  215. foo: { a: 1 },
  216. bar: markRaw({ b: 2 })
  217. })
  218. expect(isReactive(obj.foo)).toBe(true)
  219. expect(isReactive(obj.bar)).toBe(false)
  220. })
  221. test('should not observe non-extensible objects', () => {
  222. const obj = reactive({
  223. foo: Object.preventExtensions({ a: 1 }),
  224. // sealed or frozen objects are considered non-extensible as well
  225. bar: Object.freeze({ a: 1 }),
  226. baz: Object.seal({ a: 1 })
  227. })
  228. expect(isReactive(obj.foo)).toBe(false)
  229. expect(isReactive(obj.bar)).toBe(false)
  230. expect(isReactive(obj.baz)).toBe(false)
  231. })
  232. test('should not observe objects with __v_skip', () => {
  233. const original = {
  234. foo: 1,
  235. __v_skip: true
  236. }
  237. const observed = reactive(original)
  238. expect(isReactive(observed)).toBe(false)
  239. })
  240. })