readonly.spec.ts 13 KB

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