readonly.spec.ts 14 KB

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