readonly.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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. for (const [key, value] of wrapped) {
  199. expect(isReadonly(key)).toBe(true)
  200. expect(isReadonly(value)).toBe(true)
  201. }
  202. wrapped.forEach((value: any) => {
  203. expect(isReadonly(value)).toBe(true)
  204. })
  205. for (const value of wrapped.values()) {
  206. expect(isReadonly(value)).toBe(true)
  207. }
  208. })
  209. }
  210. })
  211. })
  212. const sets = [Set, WeakSet]
  213. sets.forEach((Collection: any) => {
  214. describe(Collection.name, () => {
  215. test('should make nested values readonly', () => {
  216. const key1 = {}
  217. const key2 = {}
  218. const original = new Collection([key1, key2])
  219. const wrapped = readonly(original)
  220. expect(wrapped).not.toBe(original)
  221. expect(isProxy(wrapped)).toBe(true)
  222. expect(isReactive(wrapped)).toBe(false)
  223. expect(isReadonly(wrapped)).toBe(true)
  224. expect(isReactive(original)).toBe(false)
  225. expect(isReadonly(original)).toBe(false)
  226. expect(wrapped.has(reactive(key1))).toBe(true)
  227. expect(original.has(reactive(key1))).toBe(false)
  228. })
  229. test('should not allow mutation & not trigger effect', () => {
  230. const set = readonly(new Collection())
  231. const key = {}
  232. let dummy
  233. effect(() => {
  234. dummy = set.has(key)
  235. })
  236. expect(dummy).toBe(false)
  237. set.add(key)
  238. expect(dummy).toBe(false)
  239. expect(set.has(key)).toBe(false)
  240. expect(
  241. `Add operation on key "${key}" failed: target is readonly.`
  242. ).toHaveBeenWarned()
  243. })
  244. if (Collection === Set) {
  245. test('should retrieve readonly values on iteration', () => {
  246. const original = new Collection([{}, {}])
  247. const wrapped: any = readonly(original)
  248. for (const value of wrapped) {
  249. expect(isReadonly(value)).toBe(true)
  250. }
  251. wrapped.forEach((value: any) => {
  252. expect(isReadonly(value)).toBe(true)
  253. })
  254. for (const value of wrapped.values()) {
  255. expect(isReadonly(value)).toBe(true)
  256. }
  257. for (const [v1, v2] of wrapped.entries()) {
  258. expect(isReadonly(v1)).toBe(true)
  259. expect(isReadonly(v2)).toBe(true)
  260. }
  261. })
  262. }
  263. })
  264. })
  265. test('calling reactive on an readonly should return readonly', () => {
  266. const a = readonly({})
  267. const b = reactive(a)
  268. expect(isReadonly(b)).toBe(true)
  269. // should point to same original
  270. expect(toRaw(a)).toBe(toRaw(b))
  271. })
  272. test('calling readonly on a reactive object should return readonly', () => {
  273. const a = reactive({})
  274. const b = readonly(a)
  275. expect(isReadonly(b)).toBe(true)
  276. // should point to same original
  277. expect(toRaw(a)).toBe(toRaw(b))
  278. })
  279. test('readonly should track and trigger if wrapping reactive original', () => {
  280. const a = reactive({ n: 1 })
  281. const b = readonly(a)
  282. // should return true since it's wrapping a reactive source
  283. expect(isReactive(b)).toBe(true)
  284. let dummy
  285. effect(() => {
  286. dummy = b.n
  287. })
  288. expect(dummy).toBe(1)
  289. a.n++
  290. expect(b.n).toBe(2)
  291. expect(dummy).toBe(2)
  292. })
  293. test('wrapping already wrapped value should return same Proxy', () => {
  294. const original = { foo: 1 }
  295. const wrapped = readonly(original)
  296. const wrapped2 = readonly(wrapped)
  297. expect(wrapped2).toBe(wrapped)
  298. })
  299. test('wrapping the same value multiple times should return same Proxy', () => {
  300. const original = { foo: 1 }
  301. const wrapped = readonly(original)
  302. const wrapped2 = readonly(original)
  303. expect(wrapped2).toBe(wrapped)
  304. })
  305. test('markRaw', () => {
  306. const obj = readonly({
  307. foo: { a: 1 },
  308. bar: markRaw({ b: 2 })
  309. })
  310. expect(isReadonly(obj.foo)).toBe(true)
  311. expect(isReactive(obj.bar)).toBe(false)
  312. })
  313. test('should make ref readonly', () => {
  314. const n: any = readonly(ref(1))
  315. n.value = 2
  316. expect(n.value).toBe(1)
  317. expect(
  318. `Set operation on key "value" failed: target is readonly.`
  319. ).toHaveBeenWarned()
  320. })
  321. describe('shallowReadonly', () => {
  322. test('should not make non-reactive properties reactive', () => {
  323. const props = shallowReadonly({ n: { foo: 1 } })
  324. expect(isReactive(props.n)).toBe(false)
  325. })
  326. test('should make root level properties readonly', () => {
  327. const props = shallowReadonly({ n: 1 })
  328. // @ts-ignore
  329. props.n = 2
  330. expect(props.n).toBe(1)
  331. expect(
  332. `Set operation on key "n" failed: target is readonly.`
  333. ).toHaveBeenWarned()
  334. })
  335. // to retain 2.x behavior.
  336. test('should NOT make nested properties readonly', () => {
  337. const props = shallowReadonly({ n: { foo: 1 } })
  338. // @ts-ignore
  339. props.n.foo = 2
  340. expect(props.n.foo).toBe(2)
  341. expect(
  342. `Set operation on key "foo" failed: target is readonly.`
  343. ).not.toHaveBeenWarned()
  344. })
  345. })
  346. })