readonly.spec.ts 17 KB

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