immutable.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import {
  2. reactive,
  3. immutable,
  4. toRaw,
  5. isReactive,
  6. isImmutable,
  7. markNonReactive,
  8. markImmutable,
  9. lock,
  10. unlock,
  11. effect,
  12. ref
  13. } from '../src'
  14. describe('reactivity/immutable', () => {
  15. let warn: any
  16. beforeEach(() => {
  17. warn = jest.spyOn(console, 'warn')
  18. warn.mockImplementation(() => {})
  19. })
  20. afterEach(() => {
  21. warn.mockRestore()
  22. })
  23. describe('Object', () => {
  24. it('should make nested values immutable', () => {
  25. const original = { foo: 1, bar: { baz: 2 } }
  26. const observed = immutable(original)
  27. expect(observed).not.toBe(original)
  28. expect(isReactive(observed)).toBe(true)
  29. expect(isImmutable(observed)).toBe(true)
  30. expect(isReactive(original)).toBe(false)
  31. expect(isImmutable(original)).toBe(false)
  32. expect(isReactive(observed.bar)).toBe(true)
  33. expect(isImmutable(observed.bar)).toBe(true)
  34. expect(isReactive(original.bar)).toBe(false)
  35. expect(isImmutable(original.bar)).toBe(false)
  36. // get
  37. expect(observed.foo).toBe(1)
  38. // has
  39. expect('foo' in observed).toBe(true)
  40. // ownKeys
  41. expect(Object.keys(observed)).toEqual(['foo', 'bar'])
  42. })
  43. it('should not allow mutation', () => {
  44. const observed: any = immutable({ foo: 1, bar: { baz: 2 } })
  45. observed.foo = 2
  46. expect(observed.foo).toBe(1)
  47. expect(warn).toHaveBeenCalledTimes(1)
  48. observed.bar.baz = 3
  49. expect(observed.bar.baz).toBe(2)
  50. expect(warn).toHaveBeenCalledTimes(2)
  51. delete observed.foo
  52. expect(observed.foo).toBe(1)
  53. expect(warn).toHaveBeenCalledTimes(3)
  54. delete observed.bar.baz
  55. expect(observed.bar.baz).toBe(2)
  56. expect(warn).toHaveBeenCalledTimes(4)
  57. })
  58. it('should allow mutation when unlocked', () => {
  59. const observed: any = immutable({ foo: 1, bar: { baz: 2 } })
  60. unlock()
  61. observed.prop = 2
  62. observed.bar.qux = 3
  63. delete observed.bar.baz
  64. delete observed.foo
  65. lock()
  66. expect(observed.prop).toBe(2)
  67. expect(observed.foo).toBeUndefined()
  68. expect(observed.bar.qux).toBe(3)
  69. expect('baz' in observed.bar).toBe(false)
  70. expect(warn).not.toHaveBeenCalled()
  71. })
  72. it('should not trigger effects when locked', () => {
  73. const observed: any = immutable({ a: 1 })
  74. let dummy
  75. effect(() => {
  76. dummy = observed.a
  77. })
  78. expect(dummy).toBe(1)
  79. observed.a = 2
  80. expect(observed.a).toBe(1)
  81. expect(dummy).toBe(1)
  82. })
  83. it('should trigger effects when unlocked', () => {
  84. const observed: any = immutable({ a: 1 })
  85. let dummy
  86. effect(() => {
  87. dummy = observed.a
  88. })
  89. expect(dummy).toBe(1)
  90. unlock()
  91. observed.a = 2
  92. lock()
  93. expect(observed.a).toBe(2)
  94. expect(dummy).toBe(2)
  95. })
  96. })
  97. describe('Array', () => {
  98. it('should make nested values immutable', () => {
  99. const original: any[] = [{ foo: 1 }]
  100. const observed = immutable(original)
  101. expect(observed).not.toBe(original)
  102. expect(isReactive(observed)).toBe(true)
  103. expect(isImmutable(observed)).toBe(true)
  104. expect(isReactive(original)).toBe(false)
  105. expect(isImmutable(original)).toBe(false)
  106. expect(isReactive(observed[0])).toBe(true)
  107. expect(isImmutable(observed[0])).toBe(true)
  108. expect(isReactive(original[0])).toBe(false)
  109. expect(isImmutable(original[0])).toBe(false)
  110. // get
  111. expect(observed[0].foo).toBe(1)
  112. // has
  113. expect(0 in observed).toBe(true)
  114. // ownKeys
  115. expect(Object.keys(observed)).toEqual(['0'])
  116. })
  117. it('should not allow mutation', () => {
  118. const observed: any = immutable([{ foo: 1 }])
  119. observed[0] = 1
  120. expect(observed[0]).not.toBe(1)
  121. expect(warn).toHaveBeenCalledTimes(1)
  122. observed[0].foo = 2
  123. expect(observed[0].foo).toBe(1)
  124. expect(warn).toHaveBeenCalledTimes(2)
  125. // should block length mutation
  126. observed.length = 0
  127. expect(observed.length).toBe(1)
  128. expect(observed[0].foo).toBe(1)
  129. expect(warn).toHaveBeenCalledTimes(3)
  130. // mutation methods invoke set/length internally and thus are blocked as well
  131. observed.push(2)
  132. expect(observed.length).toBe(1)
  133. // push triggers two warnings on [1] and .length
  134. expect(warn).toHaveBeenCalledTimes(5)
  135. })
  136. it('should allow mutation when unlocked', () => {
  137. const observed: any = immutable([{ foo: 1, bar: { baz: 2 } }])
  138. unlock()
  139. observed[1] = 2
  140. observed.push(3)
  141. observed[0].foo = 2
  142. observed[0].bar.baz = 3
  143. lock()
  144. expect(observed.length).toBe(3)
  145. expect(observed[1]).toBe(2)
  146. expect(observed[2]).toBe(3)
  147. expect(observed[0].foo).toBe(2)
  148. expect(observed[0].bar.baz).toBe(3)
  149. expect(warn).not.toHaveBeenCalled()
  150. })
  151. it('should not trigger effects when locked', () => {
  152. const observed: any = immutable([{ a: 1 }])
  153. let dummy
  154. effect(() => {
  155. dummy = observed[0].a
  156. })
  157. expect(dummy).toBe(1)
  158. observed[0].a = 2
  159. expect(observed[0].a).toBe(1)
  160. expect(dummy).toBe(1)
  161. observed[0] = { a: 2 }
  162. expect(observed[0].a).toBe(1)
  163. expect(dummy).toBe(1)
  164. })
  165. it('should trigger effects when unlocked', () => {
  166. const observed: any = immutable([{ a: 1 }])
  167. let dummy
  168. effect(() => {
  169. dummy = observed[0].a
  170. })
  171. expect(dummy).toBe(1)
  172. unlock()
  173. observed[0].a = 2
  174. expect(observed[0].a).toBe(2)
  175. expect(dummy).toBe(2)
  176. observed[0] = { a: 3 }
  177. expect(observed[0].a).toBe(3)
  178. expect(dummy).toBe(3)
  179. observed.unshift({ a: 4 })
  180. expect(observed[0].a).toBe(4)
  181. expect(dummy).toBe(4)
  182. lock()
  183. })
  184. })
  185. const maps = [Map, WeakMap]
  186. maps.forEach((Collection: any) => {
  187. describe(Collection.name, () => {
  188. test('should make nested values immutable', () => {
  189. const key1 = {}
  190. const key2 = {}
  191. const original = new Collection([[key1, {}], [key2, {}]])
  192. const observed = immutable(original)
  193. expect(observed).not.toBe(original)
  194. expect(isReactive(observed)).toBe(true)
  195. expect(isImmutable(observed)).toBe(true)
  196. expect(isReactive(original)).toBe(false)
  197. expect(isImmutable(original)).toBe(false)
  198. expect(isReactive(observed.get(key1))).toBe(true)
  199. expect(isImmutable(observed.get(key1))).toBe(true)
  200. expect(isReactive(original.get(key1))).toBe(false)
  201. expect(isImmutable(original.get(key1))).toBe(false)
  202. })
  203. test('should not allow mutation & not trigger effect', () => {
  204. const map = immutable(new Collection())
  205. const key = {}
  206. let dummy
  207. effect(() => {
  208. dummy = map.get(key)
  209. })
  210. expect(dummy).toBeUndefined()
  211. map.set(key, 1)
  212. expect(dummy).toBeUndefined()
  213. expect(map.has(key)).toBe(false)
  214. expect(warn).toHaveBeenCalledTimes(1)
  215. })
  216. test('should allow mutation & trigger effect when unlocked', () => {
  217. const map = immutable(new Collection())
  218. const isWeak = Collection === WeakMap
  219. const key = {}
  220. let dummy
  221. effect(() => {
  222. dummy = map.get(key) + (isWeak ? 0 : map.size)
  223. })
  224. expect(dummy).toBeNaN()
  225. unlock()
  226. map.set(key, 1)
  227. lock()
  228. expect(dummy).toBe(isWeak ? 1 : 2)
  229. expect(map.get(key)).toBe(1)
  230. expect(warn).not.toHaveBeenCalled()
  231. })
  232. if (Collection === Map) {
  233. test('should retrive immutable values on iteration', () => {
  234. const key1 = {}
  235. const key2 = {}
  236. const original = new Collection([[key1, {}], [key2, {}]])
  237. const observed: any = immutable(original)
  238. for (const [key, value] of observed) {
  239. expect(isImmutable(key)).toBe(true)
  240. expect(isImmutable(value)).toBe(true)
  241. }
  242. observed.forEach((value: any) => {
  243. expect(isImmutable(value)).toBe(true)
  244. })
  245. for (const value of observed.values()) {
  246. expect(isImmutable(value)).toBe(true)
  247. }
  248. })
  249. }
  250. })
  251. })
  252. const sets = [Set, WeakSet]
  253. sets.forEach((Collection: any) => {
  254. describe(Collection.name, () => {
  255. test('should make nested values immutable', () => {
  256. const key1 = {}
  257. const key2 = {}
  258. const original = new Collection([key1, key2])
  259. const observed = immutable(original)
  260. expect(observed).not.toBe(original)
  261. expect(isReactive(observed)).toBe(true)
  262. expect(isImmutable(observed)).toBe(true)
  263. expect(isReactive(original)).toBe(false)
  264. expect(isImmutable(original)).toBe(false)
  265. expect(observed.has(reactive(key1))).toBe(true)
  266. expect(original.has(reactive(key1))).toBe(false)
  267. })
  268. test('should not allow mutation & not trigger effect', () => {
  269. const set = immutable(new Collection())
  270. const key = {}
  271. let dummy
  272. effect(() => {
  273. dummy = set.has(key)
  274. })
  275. expect(dummy).toBe(false)
  276. set.add(key)
  277. expect(dummy).toBe(false)
  278. expect(set.has(key)).toBe(false)
  279. expect(warn).toHaveBeenCalledTimes(1)
  280. })
  281. test('should allow mutation & trigger effect when unlocked', () => {
  282. const set = immutable(new Collection())
  283. const key = {}
  284. let dummy
  285. effect(() => {
  286. dummy = set.has(key)
  287. })
  288. expect(dummy).toBe(false)
  289. unlock()
  290. set.add(key)
  291. lock()
  292. expect(dummy).toBe(true)
  293. expect(set.has(key)).toBe(true)
  294. expect(warn).not.toHaveBeenCalled()
  295. })
  296. if (Collection === Set) {
  297. test('should retrive immutable values on iteration', () => {
  298. const original = new Collection([{}, {}])
  299. const observed: any = immutable(original)
  300. for (const value of observed) {
  301. expect(isImmutable(value)).toBe(true)
  302. }
  303. observed.forEach((value: any) => {
  304. expect(isImmutable(value)).toBe(true)
  305. })
  306. for (const value of observed.values()) {
  307. expect(isImmutable(value)).toBe(true)
  308. }
  309. for (const [v1, v2] of observed.entries()) {
  310. expect(isImmutable(v1)).toBe(true)
  311. expect(isImmutable(v2)).toBe(true)
  312. }
  313. })
  314. }
  315. })
  316. })
  317. test('calling reactive on an immutable should return immutable', () => {
  318. const a = immutable({})
  319. const b = reactive(a)
  320. expect(isImmutable(b)).toBe(true)
  321. // should point to same original
  322. expect(toRaw(a)).toBe(toRaw(b))
  323. })
  324. test('calling immutable on a reactive object should return immutable', () => {
  325. const a = reactive({})
  326. const b = immutable(a)
  327. expect(isImmutable(b)).toBe(true)
  328. // should point to same original
  329. expect(toRaw(a)).toBe(toRaw(b))
  330. })
  331. test('observing already observed value should return same Proxy', () => {
  332. const original = { foo: 1 }
  333. const observed = immutable(original)
  334. const observed2 = immutable(observed)
  335. expect(observed2).toBe(observed)
  336. })
  337. test('observing the same value multiple times should return same Proxy', () => {
  338. const original = { foo: 1 }
  339. const observed = immutable(original)
  340. const observed2 = immutable(original)
  341. expect(observed2).toBe(observed)
  342. })
  343. test('markNonReactive', () => {
  344. const obj = immutable({
  345. foo: { a: 1 },
  346. bar: markNonReactive({ b: 2 })
  347. })
  348. expect(isReactive(obj.foo)).toBe(true)
  349. expect(isReactive(obj.bar)).toBe(false)
  350. })
  351. test('markImmutable', () => {
  352. const obj = reactive({
  353. foo: { a: 1 },
  354. bar: markImmutable({ b: 2 })
  355. })
  356. expect(isReactive(obj.foo)).toBe(true)
  357. expect(isReactive(obj.bar)).toBe(true)
  358. expect(isImmutable(obj.foo)).toBe(false)
  359. expect(isImmutable(obj.bar)).toBe(true)
  360. })
  361. test('should make ref immutable', () => {
  362. const n: any = immutable(ref(1))
  363. n.value = 2
  364. expect(n.value).toBe(1)
  365. expect(warn).toHaveBeenCalledTimes(1)
  366. })
  367. })