readonly.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import {
  2. reactive,
  3. readonly,
  4. toRaw,
  5. isReactive,
  6. isReadonly,
  7. markRaw,
  8. effect,
  9. ref,
  10. shallowReadonly,
  11. isProxy
  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([[key1, {}], [key2, {}]])
  166. const wrapped = readonly(original)
  167. expect(wrapped).not.toBe(original)
  168. expect(isProxy(wrapped)).toBe(true)
  169. expect(isReactive(wrapped)).toBe(false)
  170. expect(isReadonly(wrapped)).toBe(true)
  171. expect(isReactive(original)).toBe(false)
  172. expect(isReadonly(original)).toBe(false)
  173. expect(isReactive(wrapped.get(key1))).toBe(false)
  174. expect(isReadonly(wrapped.get(key1))).toBe(true)
  175. expect(isReactive(original.get(key1))).toBe(false)
  176. expect(isReadonly(original.get(key1))).toBe(false)
  177. })
  178. test('should not allow mutation & not trigger effect', () => {
  179. const map = readonly(new Collection())
  180. const key = {}
  181. let dummy
  182. effect(() => {
  183. dummy = map.get(key)
  184. })
  185. expect(dummy).toBeUndefined()
  186. map.set(key, 1)
  187. expect(dummy).toBeUndefined()
  188. expect(map.has(key)).toBe(false)
  189. expect(
  190. `Set operation on key "${key}" failed: target is readonly.`
  191. ).toHaveBeenWarned()
  192. })
  193. // #1772
  194. test('readonly + reactive should make get() value also readonly + reactive', () => {
  195. const map = reactive(new Collection())
  196. const roMap = readonly(map)
  197. const key = {}
  198. map.set(key, {})
  199. const item = map.get(key)
  200. expect(isReactive(item)).toBe(true)
  201. expect(isReadonly(item)).toBe(false)
  202. const roItem = roMap.get(key)
  203. expect(isReactive(roItem)).toBe(true)
  204. expect(isReadonly(roItem)).toBe(true)
  205. })
  206. if (Collection === Map) {
  207. test('should retrieve readonly values on iteration', () => {
  208. const key1 = {}
  209. const key2 = {}
  210. const original = new Map([[key1, {}], [key2, {}]])
  211. const wrapped: any = readonly(original)
  212. expect(wrapped.size).toBe(2)
  213. for (const [key, value] of wrapped) {
  214. expect(isReadonly(key)).toBe(true)
  215. expect(isReadonly(value)).toBe(true)
  216. }
  217. wrapped.forEach((value: any) => {
  218. expect(isReadonly(value)).toBe(true)
  219. })
  220. for (const value of wrapped.values()) {
  221. expect(isReadonly(value)).toBe(true)
  222. }
  223. })
  224. test('should retrieve reactive + readonly values on iteration', () => {
  225. const key1 = {}
  226. const key2 = {}
  227. const original = reactive(new Map([[key1, {}], [key2, {}]]))
  228. const wrapped: any = readonly(original)
  229. expect(wrapped.size).toBe(2)
  230. for (const [key, value] of wrapped) {
  231. expect(isReadonly(key)).toBe(true)
  232. expect(isReadonly(value)).toBe(true)
  233. expect(isReactive(key)).toBe(true)
  234. expect(isReactive(value)).toBe(true)
  235. }
  236. wrapped.forEach((value: any) => {
  237. expect(isReadonly(value)).toBe(true)
  238. expect(isReactive(value)).toBe(true)
  239. })
  240. for (const value of wrapped.values()) {
  241. expect(isReadonly(value)).toBe(true)
  242. expect(isReactive(value)).toBe(true)
  243. }
  244. })
  245. }
  246. })
  247. })
  248. const sets = [Set, WeakSet]
  249. sets.forEach((Collection: any) => {
  250. describe(Collection.name, () => {
  251. test('should make nested values readonly', () => {
  252. const key1 = {}
  253. const key2 = {}
  254. const original = new Collection([key1, key2])
  255. const wrapped = readonly(original)
  256. expect(wrapped).not.toBe(original)
  257. expect(isProxy(wrapped)).toBe(true)
  258. expect(isReactive(wrapped)).toBe(false)
  259. expect(isReadonly(wrapped)).toBe(true)
  260. expect(isReactive(original)).toBe(false)
  261. expect(isReadonly(original)).toBe(false)
  262. expect(wrapped.has(reactive(key1))).toBe(true)
  263. expect(original.has(reactive(key1))).toBe(false)
  264. })
  265. test('should not allow mutation & not trigger effect', () => {
  266. const set = readonly(new Collection())
  267. const key = {}
  268. let dummy
  269. effect(() => {
  270. dummy = set.has(key)
  271. })
  272. expect(dummy).toBe(false)
  273. set.add(key)
  274. expect(dummy).toBe(false)
  275. expect(set.has(key)).toBe(false)
  276. expect(
  277. `Add operation on key "${key}" failed: target is readonly.`
  278. ).toHaveBeenWarned()
  279. })
  280. if (Collection === Set) {
  281. test('should retrieve readonly values on iteration', () => {
  282. const original = new Collection([{}, {}])
  283. const wrapped: any = readonly(original)
  284. expect(wrapped.size).toBe(2)
  285. for (const value of wrapped) {
  286. expect(isReadonly(value)).toBe(true)
  287. }
  288. wrapped.forEach((value: any) => {
  289. expect(isReadonly(value)).toBe(true)
  290. })
  291. for (const value of wrapped.values()) {
  292. expect(isReadonly(value)).toBe(true)
  293. }
  294. for (const [v1, v2] of wrapped.entries()) {
  295. expect(isReadonly(v1)).toBe(true)
  296. expect(isReadonly(v2)).toBe(true)
  297. }
  298. })
  299. }
  300. })
  301. })
  302. test('calling reactive on an readonly should return readonly', () => {
  303. const a = readonly({})
  304. const b = reactive(a)
  305. expect(isReadonly(b)).toBe(true)
  306. // should point to same original
  307. expect(toRaw(a)).toBe(toRaw(b))
  308. })
  309. test('calling readonly on a reactive object should return readonly', () => {
  310. const a = reactive({})
  311. const b = readonly(a)
  312. expect(isReadonly(b)).toBe(true)
  313. // should point to same original
  314. expect(toRaw(a)).toBe(toRaw(b))
  315. })
  316. test('readonly should track and trigger if wrapping reactive original', () => {
  317. const a = reactive({ n: 1 })
  318. const b = readonly(a)
  319. // should return true since it's wrapping a reactive source
  320. expect(isReactive(b)).toBe(true)
  321. let dummy
  322. effect(() => {
  323. dummy = b.n
  324. })
  325. expect(dummy).toBe(1)
  326. a.n++
  327. expect(b.n).toBe(2)
  328. expect(dummy).toBe(2)
  329. })
  330. test('readonly collection should not track', () => {
  331. const map = new Map()
  332. map.set('foo', 1)
  333. const reMap = reactive(map)
  334. const roMap = readonly(map)
  335. let dummy
  336. effect(() => {
  337. dummy = roMap.get('foo')
  338. })
  339. expect(dummy).toBe(1)
  340. reMap.set('foo', 2)
  341. expect(roMap.get('foo')).toBe(2)
  342. // should not trigger
  343. expect(dummy).toBe(1)
  344. })
  345. test('readonly should track and trigger if wrapping reactive original (collection)', () => {
  346. const a = reactive(new Map())
  347. const b = readonly(a)
  348. // should return true since it's wrapping a reactive source
  349. expect(isReactive(b)).toBe(true)
  350. a.set('foo', 1)
  351. let dummy
  352. effect(() => {
  353. dummy = b.get('foo')
  354. })
  355. expect(dummy).toBe(1)
  356. a.set('foo', 2)
  357. expect(b.get('foo')).toBe(2)
  358. expect(dummy).toBe(2)
  359. })
  360. test('wrapping already wrapped value should return same Proxy', () => {
  361. const original = { foo: 1 }
  362. const wrapped = readonly(original)
  363. const wrapped2 = readonly(wrapped)
  364. expect(wrapped2).toBe(wrapped)
  365. })
  366. test('wrapping the same value multiple times should return same Proxy', () => {
  367. const original = { foo: 1 }
  368. const wrapped = readonly(original)
  369. const wrapped2 = readonly(original)
  370. expect(wrapped2).toBe(wrapped)
  371. })
  372. test('markRaw', () => {
  373. const obj = readonly({
  374. foo: { a: 1 },
  375. bar: markRaw({ b: 2 })
  376. })
  377. expect(isReadonly(obj.foo)).toBe(true)
  378. expect(isReactive(obj.bar)).toBe(false)
  379. })
  380. test('should make ref readonly', () => {
  381. const n: any = readonly(ref(1))
  382. n.value = 2
  383. expect(n.value).toBe(1)
  384. expect(
  385. `Set operation on key "value" failed: target is readonly.`
  386. ).toHaveBeenWarned()
  387. })
  388. describe('shallowReadonly', () => {
  389. test('should not make non-reactive properties reactive', () => {
  390. const props = shallowReadonly({ n: { foo: 1 } })
  391. expect(isReactive(props.n)).toBe(false)
  392. })
  393. test('should make root level properties readonly', () => {
  394. const props = shallowReadonly({ n: 1 })
  395. // @ts-ignore
  396. props.n = 2
  397. expect(props.n).toBe(1)
  398. expect(
  399. `Set operation on key "n" failed: target is readonly.`
  400. ).toHaveBeenWarned()
  401. })
  402. // to retain 2.x behavior.
  403. test('should NOT make nested properties readonly', () => {
  404. const props = shallowReadonly({ n: { foo: 1 } })
  405. // @ts-ignore
  406. props.n.foo = 2
  407. expect(props.n.foo).toBe(2)
  408. expect(
  409. `Set operation on key "foo" failed: target is readonly.`
  410. ).not.toHaveBeenWarned()
  411. })
  412. })
  413. })