readonly.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import {
  2. computed,
  3. effect,
  4. isProxy,
  5. isReactive,
  6. isReadonly,
  7. markRaw,
  8. reactive,
  9. readonly,
  10. ref,
  11. toRaw,
  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-expect-error
  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-expect-error
  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-expect-error
  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. test('should return undefined from Map.clear() call', () => {
  257. const wrapped = readonly(new Collection())
  258. expect(wrapped.clear()).toBeUndefined()
  259. expect(
  260. `Clear operation failed: target is readonly.`,
  261. ).toHaveBeenWarned()
  262. })
  263. }
  264. })
  265. })
  266. const sets = [Set, WeakSet]
  267. sets.forEach((Collection: any) => {
  268. describe(Collection.name, () => {
  269. test('should make nested values readonly', () => {
  270. const key1 = {}
  271. const key2 = {}
  272. const original = new Collection([key1, key2])
  273. const wrapped = readonly(original)
  274. expect(wrapped).not.toBe(original)
  275. expect(isProxy(wrapped)).toBe(true)
  276. expect(isReactive(wrapped)).toBe(false)
  277. expect(isReadonly(wrapped)).toBe(true)
  278. expect(isReactive(original)).toBe(false)
  279. expect(isReadonly(original)).toBe(false)
  280. expect(wrapped.has(reactive(key1))).toBe(true)
  281. expect(original.has(reactive(key1))).toBe(false)
  282. })
  283. test('should not allow mutation & not trigger effect', () => {
  284. const set = readonly(new Collection())
  285. const key = {}
  286. let dummy
  287. effect(() => {
  288. dummy = set.has(key)
  289. })
  290. expect(dummy).toBe(false)
  291. set.add(key)
  292. expect(dummy).toBe(false)
  293. expect(set.has(key)).toBe(false)
  294. expect(
  295. `Add operation on key "${key}" failed: target is readonly.`,
  296. ).toHaveBeenWarned()
  297. })
  298. if (Collection === Set) {
  299. test('should retrieve readonly values on iteration', () => {
  300. const original = new Collection([{}, {}])
  301. const wrapped: any = readonly(original)
  302. expect(wrapped.size).toBe(2)
  303. for (const value of wrapped) {
  304. expect(isReadonly(value)).toBe(true)
  305. }
  306. wrapped.forEach((value: any) => {
  307. expect(isReadonly(value)).toBe(true)
  308. })
  309. for (const value of wrapped.values()) {
  310. expect(isReadonly(value)).toBe(true)
  311. }
  312. for (const [v1, v2] of wrapped.entries()) {
  313. expect(isReadonly(v1)).toBe(true)
  314. expect(isReadonly(v2)).toBe(true)
  315. }
  316. })
  317. test('should return undefined from Set.clear() call', () => {
  318. const wrapped = readonly(new Collection())
  319. expect(wrapped.clear()).toBeUndefined()
  320. expect(
  321. `Clear operation failed: target is readonly.`,
  322. ).toHaveBeenWarned()
  323. })
  324. }
  325. })
  326. })
  327. test('calling reactive on an readonly should return readonly', () => {
  328. const a = readonly({})
  329. const b = reactive(a)
  330. expect(isReadonly(b)).toBe(true)
  331. // should point to same original
  332. expect(toRaw(a)).toBe(toRaw(b))
  333. })
  334. test('calling readonly on a reactive object should return readonly', () => {
  335. const a = reactive({})
  336. const b = readonly(a)
  337. expect(isReadonly(b)).toBe(true)
  338. // should point to same original
  339. expect(toRaw(a)).toBe(toRaw(b))
  340. })
  341. test('readonly should track and trigger if wrapping reactive original', () => {
  342. const a = reactive({ n: 1 })
  343. const b = readonly(a)
  344. // should return true since it's wrapping a reactive source
  345. expect(isReactive(b)).toBe(true)
  346. let dummy
  347. effect(() => {
  348. dummy = b.n
  349. })
  350. expect(dummy).toBe(1)
  351. a.n++
  352. expect(b.n).toBe(2)
  353. expect(dummy).toBe(2)
  354. })
  355. test('readonly collection should not track', () => {
  356. const map = new Map()
  357. map.set('foo', 1)
  358. const reMap = reactive(map)
  359. const roMap = readonly(map)
  360. let dummy
  361. effect(() => {
  362. dummy = roMap.get('foo')
  363. })
  364. expect(dummy).toBe(1)
  365. reMap.set('foo', 2)
  366. expect(roMap.get('foo')).toBe(2)
  367. // should not trigger
  368. expect(dummy).toBe(1)
  369. })
  370. test('readonly array should not track', () => {
  371. const arr = [1]
  372. const roArr = readonly(arr)
  373. const eff = effect(() => {
  374. roArr.includes(2)
  375. })
  376. expect(eff.effect.deps).toBeUndefined()
  377. })
  378. test('readonly should track and trigger if wrapping reactive original (collection)', () => {
  379. const a = reactive(new Map())
  380. const b = readonly(a)
  381. // should return true since it's wrapping a reactive source
  382. expect(isReactive(b)).toBe(true)
  383. a.set('foo', 1)
  384. let dummy
  385. effect(() => {
  386. dummy = b.get('foo')
  387. })
  388. expect(dummy).toBe(1)
  389. a.set('foo', 2)
  390. expect(b.get('foo')).toBe(2)
  391. expect(dummy).toBe(2)
  392. })
  393. test('wrapping already wrapped value should return same Proxy', () => {
  394. const original = { foo: 1 }
  395. const wrapped = readonly(original)
  396. const wrapped2 = readonly(wrapped)
  397. expect(wrapped2).toBe(wrapped)
  398. })
  399. test('wrapping the same value multiple times should return same Proxy', () => {
  400. const original = { foo: 1 }
  401. const wrapped = readonly(original)
  402. const wrapped2 = readonly(original)
  403. expect(wrapped2).toBe(wrapped)
  404. })
  405. test('markRaw', () => {
  406. const obj = readonly({
  407. foo: { a: 1 },
  408. bar: markRaw({ b: 2 }),
  409. })
  410. expect(isReadonly(obj.foo)).toBe(true)
  411. expect(isReadonly(obj.bar)).toBe(false)
  412. })
  413. test('should make ref readonly', () => {
  414. const n = readonly(ref(1))
  415. // @ts-expect-error
  416. n.value = 2
  417. expect(n.value).toBe(1)
  418. expect(
  419. `Set operation on key "value" failed: target is readonly.`,
  420. ).toHaveBeenWarned()
  421. })
  422. // https://github.com/vuejs/core/issues/3376
  423. test('calling readonly on computed should allow computed to set its private properties', () => {
  424. const r = ref<boolean>(false)
  425. const c = computed(() => r.value)
  426. const rC = readonly(c)
  427. r.value = true
  428. expect(rC.value).toBe(true)
  429. expect(
  430. 'Set operation on key "_dirty" failed: target is readonly.',
  431. ).not.toHaveBeenWarned()
  432. // @ts-expect-error - non-existent property
  433. rC.randomProperty = true
  434. expect(
  435. 'Set operation on key "randomProperty" failed: target is readonly.',
  436. ).toHaveBeenWarned()
  437. })
  438. // #4986
  439. test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
  440. const r = readonly({})
  441. const rr = reactive({}) as any
  442. rr.foo = r
  443. expect(rr.foo).toBe(r)
  444. expect(isReadonly(rr.foo)).toBe(true)
  445. })
  446. test('attempting to write plain value to a readonly ref nested in a reactive object should fail', () => {
  447. const r = ref(false)
  448. const ror = readonly(r)
  449. const obj = reactive({ ror })
  450. expect(() => {
  451. obj.ror = true
  452. }).toThrow()
  453. expect(obj.ror).toBe(false)
  454. })
  455. test('replacing a readonly ref nested in a reactive object with a new ref', () => {
  456. const r = ref(false)
  457. const ror = readonly(r)
  458. const obj = reactive({ ror })
  459. obj.ror = ref(true) as unknown as boolean
  460. expect(obj.ror).toBe(true)
  461. expect(toRaw(obj).ror).not.toBe(ror) // ref successfully replaced
  462. })
  463. test('setting readonly object to writable nested ref', () => {
  464. const r = ref<any>()
  465. const obj = reactive({ r })
  466. const ro = readonly({})
  467. obj.r = ro
  468. expect(obj.r).toBe(ro)
  469. expect(r.value).toBe(ro)
  470. })
  471. })