readonly.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  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. })
  161. const maps = [Map, WeakMap]
  162. maps.forEach((Collection: any) => {
  163. describe(Collection.name, () => {
  164. test('should make nested values readonly', () => {
  165. const key1 = {}
  166. const key2 = {}
  167. const original = new Collection([
  168. [key1, {}],
  169. [key2, {}],
  170. ])
  171. const wrapped = readonly(original)
  172. expect(wrapped).not.toBe(original)
  173. expect(isProxy(wrapped)).toBe(true)
  174. expect(isReactive(wrapped)).toBe(false)
  175. expect(isReadonly(wrapped)).toBe(true)
  176. expect(isReactive(original)).toBe(false)
  177. expect(isReadonly(original)).toBe(false)
  178. expect(isReactive(wrapped.get(key1))).toBe(false)
  179. expect(isReadonly(wrapped.get(key1))).toBe(true)
  180. expect(isReactive(original.get(key1))).toBe(false)
  181. expect(isReadonly(original.get(key1))).toBe(false)
  182. })
  183. test('should not allow mutation & not trigger effect', () => {
  184. const map = readonly(new Collection())
  185. const key = {}
  186. let dummy
  187. effect(() => {
  188. dummy = map.get(key)
  189. })
  190. expect(dummy).toBeUndefined()
  191. map.set(key, 1)
  192. expect(dummy).toBeUndefined()
  193. expect(map.has(key)).toBe(false)
  194. expect(
  195. `Set operation on key "${key}" failed: target is readonly.`,
  196. ).toHaveBeenWarned()
  197. })
  198. // #1772
  199. test('readonly + reactive should make get() value also readonly + reactive', () => {
  200. const map = reactive(new Collection())
  201. const roMap = readonly(map)
  202. const key = {}
  203. map.set(key, {})
  204. const item = map.get(key)
  205. expect(isReactive(item)).toBe(true)
  206. expect(isReadonly(item)).toBe(false)
  207. const roItem = roMap.get(key)
  208. expect(isReactive(roItem)).toBe(true)
  209. expect(isReadonly(roItem)).toBe(true)
  210. })
  211. if (Collection === Map) {
  212. test('should retrieve readonly values on iteration', () => {
  213. const key1 = {}
  214. const key2 = {}
  215. const original = new Map([
  216. [key1, {}],
  217. [key2, {}],
  218. ])
  219. const wrapped: any = readonly(original)
  220. expect(wrapped.size).toBe(2)
  221. for (const [key, value] of wrapped) {
  222. expect(isReadonly(key)).toBe(true)
  223. expect(isReadonly(value)).toBe(true)
  224. }
  225. wrapped.forEach((value: any) => {
  226. expect(isReadonly(value)).toBe(true)
  227. })
  228. for (const value of wrapped.values()) {
  229. expect(isReadonly(value)).toBe(true)
  230. }
  231. })
  232. test('should retrieve reactive + readonly values on iteration', () => {
  233. const key1 = {}
  234. const key2 = {}
  235. const original = reactive(
  236. new Map([
  237. [key1, {}],
  238. [key2, {}],
  239. ]),
  240. )
  241. const wrapped: any = readonly(original)
  242. expect(wrapped.size).toBe(2)
  243. for (const [key, value] of wrapped) {
  244. expect(isReadonly(key)).toBe(true)
  245. expect(isReadonly(value)).toBe(true)
  246. expect(isReactive(key)).toBe(true)
  247. expect(isReactive(value)).toBe(true)
  248. }
  249. wrapped.forEach((value: any) => {
  250. expect(isReadonly(value)).toBe(true)
  251. expect(isReactive(value)).toBe(true)
  252. })
  253. for (const value of wrapped.values()) {
  254. expect(isReadonly(value)).toBe(true)
  255. expect(isReactive(value)).toBe(true)
  256. }
  257. })
  258. test('should return undefined from Map.clear() call', () => {
  259. const wrapped = readonly(new Collection())
  260. expect(wrapped.clear()).toBeUndefined()
  261. expect(
  262. `Clear operation failed: target is readonly.`,
  263. ).toHaveBeenWarned()
  264. })
  265. }
  266. })
  267. })
  268. const sets = [Set, WeakSet]
  269. sets.forEach((Collection: any) => {
  270. describe(Collection.name, () => {
  271. test('should make nested values readonly', () => {
  272. const key1 = {}
  273. const key2 = {}
  274. const original = new Collection([key1, key2])
  275. const wrapped = readonly(original)
  276. expect(wrapped).not.toBe(original)
  277. expect(isProxy(wrapped)).toBe(true)
  278. expect(isReactive(wrapped)).toBe(false)
  279. expect(isReadonly(wrapped)).toBe(true)
  280. expect(isReactive(original)).toBe(false)
  281. expect(isReadonly(original)).toBe(false)
  282. expect(wrapped.has(reactive(key1))).toBe(true)
  283. expect(original.has(reactive(key1))).toBe(false)
  284. })
  285. test('should not allow mutation & not trigger effect', () => {
  286. const set = readonly(new Collection())
  287. const key = {}
  288. let dummy
  289. effect(() => {
  290. dummy = set.has(key)
  291. })
  292. expect(dummy).toBe(false)
  293. set.add(key)
  294. expect(dummy).toBe(false)
  295. expect(set.has(key)).toBe(false)
  296. expect(
  297. `Add operation on key "${key}" failed: target is readonly.`,
  298. ).toHaveBeenWarned()
  299. })
  300. if (Collection === Set) {
  301. test('should retrieve readonly values on iteration', () => {
  302. const original = new Collection([{}, {}])
  303. const wrapped: any = readonly(original)
  304. expect(wrapped.size).toBe(2)
  305. for (const value of wrapped) {
  306. expect(isReadonly(value)).toBe(true)
  307. }
  308. wrapped.forEach((value: any) => {
  309. expect(isReadonly(value)).toBe(true)
  310. })
  311. for (const value of wrapped.values()) {
  312. expect(isReadonly(value)).toBe(true)
  313. }
  314. for (const [v1, v2] of wrapped.entries()) {
  315. expect(isReadonly(v1)).toBe(true)
  316. expect(isReadonly(v2)).toBe(true)
  317. }
  318. })
  319. test('should return undefined from Set.clear() call', () => {
  320. const wrapped = readonly(new Collection())
  321. expect(wrapped.clear()).toBeUndefined()
  322. expect(
  323. `Clear operation failed: target is readonly.`,
  324. ).toHaveBeenWarned()
  325. })
  326. }
  327. })
  328. })
  329. test('calling reactive on an readonly should return readonly', () => {
  330. const a = readonly({})
  331. const b = reactive(a)
  332. expect(isReadonly(b)).toBe(true)
  333. // should point to same original
  334. expect(toRaw(a)).toBe(toRaw(b))
  335. })
  336. test('calling readonly on a reactive object should return readonly', () => {
  337. const a = reactive({})
  338. const b = readonly(a)
  339. expect(isReadonly(b)).toBe(true)
  340. // should point to same original
  341. expect(toRaw(a)).toBe(toRaw(b))
  342. })
  343. test('readonly should track and trigger if wrapping reactive original', () => {
  344. const a = reactive({ n: 1 })
  345. const b = readonly(a)
  346. // should return true since it's wrapping a reactive source
  347. expect(isReactive(b)).toBe(true)
  348. let dummy
  349. effect(() => {
  350. dummy = b.n
  351. })
  352. expect(dummy).toBe(1)
  353. a.n++
  354. expect(b.n).toBe(2)
  355. expect(dummy).toBe(2)
  356. })
  357. test('readonly collection should not track', () => {
  358. const map = new Map()
  359. map.set('foo', 1)
  360. const reMap = reactive(map)
  361. const roMap = readonly(map)
  362. let dummy
  363. effect(() => {
  364. dummy = roMap.get('foo')
  365. })
  366. expect(dummy).toBe(1)
  367. reMap.set('foo', 2)
  368. expect(roMap.get('foo')).toBe(2)
  369. // should not trigger
  370. expect(dummy).toBe(1)
  371. })
  372. test('readonly array should not track', () => {
  373. const arr = [1]
  374. const roArr = readonly(arr)
  375. const eff = effect(() => {
  376. roArr.includes(2)
  377. })
  378. expect(eff.effect.deps).toBeUndefined()
  379. })
  380. test('readonly should track and trigger if wrapping reactive original (collection)', () => {
  381. const a = reactive(new Map())
  382. const b = readonly(a)
  383. // should return true since it's wrapping a reactive source
  384. expect(isReactive(b)).toBe(true)
  385. a.set('foo', 1)
  386. let dummy
  387. effect(() => {
  388. dummy = b.get('foo')
  389. })
  390. expect(dummy).toBe(1)
  391. a.set('foo', 2)
  392. expect(b.get('foo')).toBe(2)
  393. expect(dummy).toBe(2)
  394. })
  395. test('wrapping already wrapped value should return same Proxy', () => {
  396. const original = { foo: 1 }
  397. const wrapped = readonly(original)
  398. const wrapped2 = readonly(wrapped)
  399. expect(wrapped2).toBe(wrapped)
  400. })
  401. test('wrapping the same value multiple times should return same Proxy', () => {
  402. const original = { foo: 1 }
  403. const wrapped = readonly(original)
  404. const wrapped2 = readonly(original)
  405. expect(wrapped2).toBe(wrapped)
  406. })
  407. test('markRaw', () => {
  408. const obj = readonly({
  409. foo: { a: 1 },
  410. bar: markRaw({ b: 2 }),
  411. })
  412. expect(isReadonly(obj.foo)).toBe(true)
  413. expect(isReadonly(obj.bar)).toBe(false)
  414. })
  415. test('should make ref readonly', () => {
  416. const n = readonly(ref(1))
  417. // @ts-expect-error
  418. n.value = 2
  419. expect(n.value).toBe(1)
  420. expect(
  421. `Set operation on key "value" failed: target is readonly.`,
  422. ).toHaveBeenWarned()
  423. })
  424. // https://github.com/vuejs/core/issues/3376
  425. test('calling readonly on computed should allow computed to set its private properties', () => {
  426. const r = ref<boolean>(false)
  427. const c = computed(() => r.value)
  428. const rC = readonly(c)
  429. r.value = true
  430. expect(rC.value).toBe(true)
  431. expect(
  432. 'Set operation on key "_dirty" failed: target is readonly.',
  433. ).not.toHaveBeenWarned()
  434. // @ts-expect-error - non-existent property
  435. rC.randomProperty = true
  436. expect(
  437. 'Set operation on key "randomProperty" failed: target is readonly.',
  438. ).toHaveBeenWarned()
  439. })
  440. // #4986
  441. test('setting a readonly object as a property of a reactive object should retain readonly proxy', () => {
  442. const r = readonly({})
  443. const rr = reactive({}) as any
  444. rr.foo = r
  445. expect(rr.foo).toBe(r)
  446. expect(isReadonly(rr.foo)).toBe(true)
  447. })
  448. test('attempting to write plain value to a readonly ref nested in a reactive object should fail', () => {
  449. const r = ref(false)
  450. const ror = readonly(r)
  451. const obj = reactive({ ror })
  452. obj.ror = true
  453. expect(
  454. `Set operation on key "ror" failed: target is readonly.`,
  455. ).toHaveBeenWarned()
  456. expect(obj.ror).toBe(false)
  457. })
  458. test('replacing a readonly ref nested in a reactive object with a new ref', () => {
  459. const r = ref(false)
  460. const ror = readonly(r)
  461. const obj = reactive({ ror })
  462. obj.ror = ref(true) as unknown as boolean
  463. expect(obj.ror).toBe(true)
  464. expect(toRaw(obj).ror).not.toBe(ror) // ref successfully replaced
  465. })
  466. test('setting readonly object to writable nested ref', () => {
  467. const r = ref<any>()
  468. const obj = reactive({ r })
  469. const ro = readonly({})
  470. obj.r = ro
  471. expect(obj.r).toBe(ro)
  472. expect(r.value).toBe(ro)
  473. })
  474. test('should keep nested ref readonly', () => {
  475. const items = ref(['one', 'two', 'three'])
  476. const obj = {
  477. o: readonly({
  478. items,
  479. }),
  480. }
  481. expect(isReadonly(obj.o.items)).toBe(true)
  482. })
  483. })
  484. test('should be able to trigger with triggerRef', () => {
  485. const r = shallowRef({ a: 1 })
  486. const ror = readonly(r)
  487. let dummy
  488. effect(() => {
  489. dummy = ror.value.a
  490. })
  491. r.value.a = 2
  492. expect(dummy).toBe(1)
  493. triggerRef(ror)
  494. expect(dummy).toBe(2)
  495. })