readonly.spec.ts 13 KB

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