readonly.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. import {
  2. computed,
  3. effect,
  4. isProxy,
  5. isReactive,
  6. isReadonly,
  7. markRaw,
  8. reactive,
  9. readonly,
  10. ref,
  11. shallowRef,
  12. toRaw,
  13. triggerRef,
  14. } from '../src'
  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. 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. // @ts-expect-error
  67. delete wrapped.foo
  68. expect(wrapped.foo).toBe(1)
  69. expect(
  70. `Delete operation on key "foo" failed: target is readonly.`,
  71. ).toHaveBeenWarnedLast()
  72. // @ts-expect-error
  73. delete wrapped.bar.baz
  74. expect(wrapped.bar.baz).toBe(2)
  75. expect(
  76. `Delete operation on key "baz" failed: target is readonly.`,
  77. ).toHaveBeenWarnedLast()
  78. // @ts-expect-error
  79. delete wrapped[qux]
  80. expect(wrapped[qux]).toBe(3)
  81. expect(
  82. `Delete operation on key "Symbol(qux)" failed: target is readonly.`,
  83. ).toHaveBeenWarnedLast()
  84. })
  85. it('should not trigger effects', () => {
  86. const wrapped: any = readonly({ a: 1 })
  87. let dummy
  88. effect(() => {
  89. dummy = wrapped.a
  90. })
  91. expect(dummy).toBe(1)
  92. wrapped.a = 2
  93. expect(wrapped.a).toBe(1)
  94. expect(dummy).toBe(1)
  95. expect(`target is readonly`).toHaveBeenWarned()
  96. })
  97. })
  98. describe('Array', () => {
  99. it('should make nested values readonly', () => {
  100. const original = [{ foo: 1 }]
  101. const wrapped = readonly(original)
  102. expect(wrapped).not.toBe(original)
  103. expect(isProxy(wrapped)).toBe(true)
  104. expect(isReactive(wrapped)).toBe(false)
  105. expect(isReadonly(wrapped)).toBe(true)
  106. expect(isReactive(original)).toBe(false)
  107. expect(isReadonly(original)).toBe(false)
  108. expect(isReactive(wrapped[0])).toBe(false)
  109. expect(isReadonly(wrapped[0])).toBe(true)
  110. expect(isReactive(original[0])).toBe(false)
  111. expect(isReadonly(original[0])).toBe(false)
  112. // get
  113. expect(wrapped[0].foo).toBe(1)
  114. // has
  115. expect(0 in wrapped).toBe(true)
  116. // ownKeys
  117. expect(Object.keys(wrapped)).toEqual(['0'])
  118. })
  119. it('should not allow mutation', () => {
  120. const wrapped: any = readonly([{ foo: 1 }])
  121. wrapped[0] = 1
  122. expect(wrapped[0]).not.toBe(1)
  123. expect(
  124. `Set operation on key "0" failed: target is readonly.`,
  125. ).toHaveBeenWarned()
  126. wrapped[0].foo = 2
  127. expect(wrapped[0].foo).toBe(1)
  128. expect(
  129. `Set operation on key "foo" failed: target is readonly.`,
  130. ).toHaveBeenWarned()
  131. // should block length mutation
  132. wrapped.length = 0
  133. expect(wrapped.length).toBe(1)
  134. expect(wrapped[0].foo).toBe(1)
  135. expect(
  136. `Set operation on key "length" failed: target is readonly.`,
  137. ).toHaveBeenWarned()
  138. // mutation methods invoke set/length internally and thus are blocked as well
  139. wrapped.push(2)
  140. expect(wrapped.length).toBe(1)
  141. // push triggers two warnings on [1] and .length
  142. expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
  143. })
  144. it('should not trigger effects', () => {
  145. const wrapped: any = readonly([{ a: 1 }])
  146. let dummy
  147. effect(() => {
  148. dummy = wrapped[0].a
  149. })
  150. expect(dummy).toBe(1)
  151. wrapped[0].a = 2
  152. expect(wrapped[0].a).toBe(1)
  153. expect(dummy).toBe(1)
  154. expect(`target is readonly`).toHaveBeenWarnedTimes(1)
  155. wrapped[0] = { a: 2 }
  156. expect(wrapped[0].a).toBe(1)
  157. expect(dummy).toBe(1)
  158. expect(`target is readonly`).toHaveBeenWarnedTimes(2)
  159. })
  160. it('should maintain identity when iterating readonly ref array', () => {
  161. const list = readonly(ref([{}, {}, {}]))
  162. const computedList = computed(() => {
  163. const newList: any[] = []
  164. list.value.forEach(x => newList.push(x))
  165. return newList
  166. })
  167. expect(list.value[0]).toBe(computedList.value[0])
  168. expect(isReadonly(computedList.value[0])).toBe(true)
  169. expect(isReactive(computedList.value[0])).toBe(true)
  170. })
  171. })
  172. const maps = [Map, WeakMap]
  173. maps.forEach((Collection: any) => {
  174. describe(Collection.name, () => {
  175. test('should make nested values readonly', () => {
  176. const key1 = {}
  177. const key2 = {}
  178. const original = new Collection([
  179. [key1, {}],
  180. [key2, {}],
  181. ])
  182. const wrapped = readonly(original)
  183. expect(wrapped).not.toBe(original)
  184. expect(isProxy(wrapped)).toBe(true)
  185. expect(isReactive(wrapped)).toBe(false)
  186. expect(isReadonly(wrapped)).toBe(true)
  187. expect(isReactive(original)).toBe(false)
  188. expect(isReadonly(original)).toBe(false)
  189. expect(isReactive(wrapped.get(key1))).toBe(false)
  190. expect(isReadonly(wrapped.get(key1))).toBe(true)
  191. expect(isReactive(original.get(key1))).toBe(false)
  192. expect(isReadonly(original.get(key1))).toBe(false)
  193. })
  194. test('should not allow mutation & not trigger effect', () => {
  195. const map = readonly(new Collection())
  196. const key = {}
  197. let dummy
  198. effect(() => {
  199. dummy = map.get(key)
  200. })
  201. expect(dummy).toBeUndefined()
  202. map.set(key, 1)
  203. expect(dummy).toBeUndefined()
  204. expect(map.has(key)).toBe(false)
  205. expect(
  206. `Set operation on key "${key}" failed: target is readonly.`,
  207. ).toHaveBeenWarned()
  208. })
  209. // #1772
  210. test('readonly + reactive should make get() value also readonly + reactive', () => {
  211. const map = reactive(new Collection())
  212. const roMap = readonly(map)
  213. const key = {}
  214. map.set(key, {})
  215. const item = map.get(key)
  216. expect(isReactive(item)).toBe(true)
  217. expect(isReadonly(item)).toBe(false)
  218. const roItem = roMap.get(key)
  219. expect(isReactive(roItem)).toBe(true)
  220. expect(isReadonly(roItem)).toBe(true)
  221. })
  222. if (Collection === Map) {
  223. test('should retrieve readonly values on iteration', () => {
  224. const key1 = {}
  225. const key2 = {}
  226. const original = new Map([
  227. [key1, {}],
  228. [key2, {}],
  229. ])
  230. const wrapped: any = readonly(original)
  231. expect(wrapped.size).toBe(2)
  232. for (const [key, value] of wrapped) {
  233. expect(isReadonly(key)).toBe(true)
  234. expect(isReadonly(value)).toBe(true)
  235. }
  236. wrapped.forEach((value: any) => {
  237. expect(isReadonly(value)).toBe(true)
  238. })
  239. for (const value of wrapped.values()) {
  240. expect(isReadonly(value)).toBe(true)
  241. }
  242. })
  243. test('should retrieve reactive + readonly values on iteration', () => {
  244. const key1 = {}
  245. const key2 = {}
  246. const original = reactive(
  247. new Map([
  248. [key1, {}],
  249. [key2, {}],
  250. ]),
  251. )
  252. const wrapped: any = readonly(original)
  253. expect(wrapped.size).toBe(2)
  254. for (const [key, value] of wrapped) {
  255. expect(isReadonly(key)).toBe(true)
  256. expect(isReadonly(value)).toBe(true)
  257. expect(isReactive(key)).toBe(true)
  258. expect(isReactive(value)).toBe(true)
  259. }
  260. wrapped.forEach((value: any) => {
  261. expect(isReadonly(value)).toBe(true)
  262. expect(isReactive(value)).toBe(true)
  263. })
  264. for (const value of wrapped.values()) {
  265. expect(isReadonly(value)).toBe(true)
  266. expect(isReactive(value)).toBe(true)
  267. }
  268. })
  269. test('should return undefined from Map.clear() call', () => {
  270. const wrapped = readonly(new Collection())
  271. expect(wrapped.clear()).toBeUndefined()
  272. expect(
  273. `Clear operation failed: target is readonly.`,
  274. ).toHaveBeenWarned()
  275. })
  276. }
  277. })
  278. })
  279. const sets = [Set, WeakSet]
  280. sets.forEach((Collection: any) => {
  281. describe(Collection.name, () => {
  282. test('should make nested values readonly', () => {
  283. const key1 = {}
  284. const key2 = {}
  285. const original = new Collection([key1, key2])
  286. const wrapped = readonly(original)
  287. expect(wrapped).not.toBe(original)
  288. expect(isProxy(wrapped)).toBe(true)
  289. expect(isReactive(wrapped)).toBe(false)
  290. expect(isReadonly(wrapped)).toBe(true)
  291. expect(isReactive(original)).toBe(false)
  292. expect(isReadonly(original)).toBe(false)
  293. expect(wrapped.has(reactive(key1))).toBe(true)
  294. expect(original.has(reactive(key1))).toBe(false)
  295. })
  296. test('should not allow mutation & not trigger effect', () => {
  297. const set = readonly(new Collection())
  298. const key = {}
  299. let dummy
  300. effect(() => {
  301. dummy = set.has(key)
  302. })
  303. expect(dummy).toBe(false)
  304. set.add(key)
  305. expect(dummy).toBe(false)
  306. expect(set.has(key)).toBe(false)
  307. expect(
  308. `Add operation on key "${key}" failed: target is readonly.`,
  309. ).toHaveBeenWarned()
  310. })
  311. if (Collection === Set) {
  312. test('should retrieve readonly values on iteration', () => {
  313. const original = new Collection([{}, {}])
  314. const wrapped: any = readonly(original)
  315. expect(wrapped.size).toBe(2)
  316. for (const value of wrapped) {
  317. expect(isReadonly(value)).toBe(true)
  318. }
  319. wrapped.forEach((value: any) => {
  320. expect(isReadonly(value)).toBe(true)
  321. })
  322. for (const value of wrapped.values()) {
  323. expect(isReadonly(value)).toBe(true)
  324. }
  325. for (const [v1, v2] of wrapped.entries()) {
  326. expect(isReadonly(v1)).toBe(true)
  327. expect(isReadonly(v2)).toBe(true)
  328. }
  329. })
  330. test('should return undefined from Set.clear() call', () => {
  331. const wrapped = readonly(new Collection())
  332. expect(wrapped.clear()).toBeUndefined()
  333. expect(
  334. `Clear operation failed: target is readonly.`,
  335. ).toHaveBeenWarned()
  336. })
  337. }
  338. })
  339. })
  340. test('calling reactive on an readonly should return readonly', () => {
  341. const a = readonly({})
  342. const b = reactive(a)
  343. expect(isReadonly(b)).toBe(true)
  344. // should point to same original
  345. expect(toRaw(a)).toBe(toRaw(b))
  346. })
  347. test('calling readonly on a reactive object should return readonly', () => {
  348. const a = reactive({})
  349. const b = readonly(a)
  350. expect(isReadonly(b)).toBe(true)
  351. // should point to same original
  352. expect(toRaw(a)).toBe(toRaw(b))
  353. })
  354. test('readonly should track and trigger if wrapping reactive original', () => {
  355. const a = reactive({ n: 1 })
  356. const b = readonly(a)
  357. // should return true since it's wrapping a reactive source
  358. expect(isReactive(b)).toBe(true)
  359. let dummy
  360. effect(() => {
  361. dummy = b.n
  362. })
  363. expect(dummy).toBe(1)
  364. a.n++
  365. expect(b.n).toBe(2)
  366. expect(dummy).toBe(2)
  367. })
  368. test('readonly collection should not track', () => {
  369. const map = new Map()
  370. map.set('foo', 1)
  371. const reMap = reactive(map)
  372. const roMap = readonly(map)
  373. let dummy
  374. effect(() => {
  375. dummy = roMap.get('foo')
  376. })
  377. expect(dummy).toBe(1)
  378. reMap.set('foo', 2)
  379. expect(roMap.get('foo')).toBe(2)
  380. // should not trigger
  381. expect(dummy).toBe(1)
  382. })
  383. test('readonly array should not track', () => {
  384. const arr = [1]
  385. const roArr = readonly(arr)
  386. const eff = effect(() => {
  387. roArr.includes(2)
  388. })
  389. expect(eff.effect.deps).toBeUndefined()
  390. })
  391. test('readonly should track and trigger if wrapping reactive original (collection)', () => {
  392. const a = reactive(new Map())
  393. const b = readonly(a)
  394. // should return true since it's wrapping a reactive source
  395. expect(isReactive(b)).toBe(true)
  396. a.set('foo', 1)
  397. let dummy
  398. effect(() => {
  399. dummy = b.get('foo')
  400. })
  401. expect(dummy).toBe(1)
  402. a.set('foo', 2)
  403. expect(b.get('foo')).toBe(2)
  404. expect(dummy).toBe(2)
  405. })
  406. test('wrapping already wrapped value should return same Proxy', () => {
  407. const original = { foo: 1 }
  408. const wrapped = readonly(original)
  409. const wrapped2 = readonly(wrapped)
  410. expect(wrapped2).toBe(wrapped)
  411. })
  412. test('wrapping the same value multiple times should return same Proxy', () => {
  413. const original = { foo: 1 }
  414. const wrapped = readonly(original)
  415. const wrapped2 = readonly(original)
  416. expect(wrapped2).toBe(wrapped)
  417. })
  418. test('markRaw', () => {
  419. const obj = readonly({
  420. foo: { a: 1 },
  421. bar: markRaw({ b: 2 }),
  422. })
  423. expect(isReadonly(obj.foo)).toBe(true)
  424. expect(isReadonly(obj.bar)).toBe(false)
  425. })
  426. test('should make ref readonly', () => {
  427. const n = readonly(ref(1))
  428. // @ts-expect-error
  429. n.value = 2
  430. expect(n.value).toBe(1)
  431. expect(
  432. `Set operation on key "value" failed: target is readonly.`,
  433. ).toHaveBeenWarned()
  434. })
  435. // https://github.com/vuejs/core/issues/3376
  436. test('calling readonly on computed should allow computed to set its private properties', () => {
  437. const r = ref<boolean>(false)
  438. const c = computed(() => r.value)
  439. const rC = readonly(c)
  440. r.value = true
  441. expect(rC.value).toBe(true)
  442. expect(
  443. 'Set operation on key "_dirty" failed: target is readonly.',
  444. ).not.toHaveBeenWarned()
  445. // @ts-expect-error - non-existent property
  446. rC.randomProperty = true
  447. expect(
  448. 'Set operation on key "randomProperty" failed: target is readonly.',
  449. ).toHaveBeenWarned()
  450. })
  451. // #4986
  452. test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
  453. const r = readonly({})
  454. const rr = reactive({}) as any
  455. rr.foo = r
  456. expect(rr.foo).toBe(r)
  457. expect(isReadonly(rr.foo)).toBe(true)
  458. })
  459. test('attempting to write plain value to a readonly ref nested in a reactive object should fail', () => {
  460. const r = ref(false)
  461. const ror = readonly(r)
  462. const obj = reactive({ ror })
  463. obj.ror = true
  464. expect(
  465. `Set operation on key "ror" failed: target is readonly.`,
  466. ).toHaveBeenWarned()
  467. expect(obj.ror).toBe(false)
  468. })
  469. test('replacing a readonly ref nested in a reactive object with a new ref', () => {
  470. const r = ref(false)
  471. const ror = readonly(r)
  472. const obj = reactive({ ror })
  473. obj.ror = ref(true) as unknown as boolean
  474. expect(obj.ror).toBe(true)
  475. expect(toRaw(obj).ror).not.toBe(ror) // ref successfully replaced
  476. })
  477. test('setting readonly object to writable nested ref', () => {
  478. const r = ref<any>()
  479. const obj = reactive({ r })
  480. const ro = readonly({})
  481. obj.r = ro
  482. expect(obj.r).toBe(ro)
  483. expect(r.value).toBe(ro)
  484. })
  485. test('should keep nested ref readonly', () => {
  486. const items = ref(['one', 'two', 'three'])
  487. const obj = {
  488. o: readonly({
  489. items,
  490. }),
  491. }
  492. expect(isReadonly(obj.o.items)).toBe(true)
  493. })
  494. })
  495. test('should be able to trigger with triggerRef', () => {
  496. const r = shallowRef({ a: 1 })
  497. const ror = readonly(r)
  498. let dummy
  499. effect(() => {
  500. dummy = ror.value.a
  501. })
  502. r.value.a = 2
  503. expect(dummy).toBe(1)
  504. triggerRef(ror)
  505. expect(dummy).toBe(2)
  506. })