readonly.spec.ts 11 KB

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