readonly.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. delete wrapped.foo
  65. expect(wrapped.foo).toBe(1)
  66. expect(
  67. `Delete operation on key "foo" failed: target is readonly.`
  68. ).toHaveBeenWarnedLast()
  69. delete wrapped.bar.baz
  70. expect(wrapped.bar.baz).toBe(2)
  71. expect(
  72. `Delete operation on key "baz" failed: target is readonly.`
  73. ).toHaveBeenWarnedLast()
  74. delete wrapped[qux]
  75. expect(wrapped[qux]).toBe(3)
  76. expect(
  77. `Delete operation on key "Symbol(qux)" failed: target is readonly.`
  78. ).toHaveBeenWarnedLast()
  79. })
  80. it('should not trigger effects', () => {
  81. const wrapped: any = readonly({ a: 1 })
  82. let dummy
  83. effect(() => {
  84. dummy = wrapped.a
  85. })
  86. expect(dummy).toBe(1)
  87. wrapped.a = 2
  88. expect(wrapped.a).toBe(1)
  89. expect(dummy).toBe(1)
  90. expect(`target is readonly`).toHaveBeenWarned()
  91. })
  92. })
  93. describe('Array', () => {
  94. it('should make nested values readonly', () => {
  95. const original = [{ foo: 1 }]
  96. const wrapped = readonly(original)
  97. expect(wrapped).not.toBe(original)
  98. expect(isProxy(wrapped)).toBe(true)
  99. expect(isReactive(wrapped)).toBe(false)
  100. expect(isReadonly(wrapped)).toBe(true)
  101. expect(isReactive(original)).toBe(false)
  102. expect(isReadonly(original)).toBe(false)
  103. expect(isReactive(wrapped[0])).toBe(false)
  104. expect(isReadonly(wrapped[0])).toBe(true)
  105. expect(isReactive(original[0])).toBe(false)
  106. expect(isReadonly(original[0])).toBe(false)
  107. // get
  108. expect(wrapped[0].foo).toBe(1)
  109. // has
  110. expect(0 in wrapped).toBe(true)
  111. // ownKeys
  112. expect(Object.keys(wrapped)).toEqual(['0'])
  113. })
  114. it('should not allow mutation', () => {
  115. const wrapped: any = readonly([{ foo: 1 }])
  116. wrapped[0] = 1
  117. expect(wrapped[0]).not.toBe(1)
  118. expect(
  119. `Set operation on key "0" failed: target is readonly.`
  120. ).toHaveBeenWarned()
  121. wrapped[0].foo = 2
  122. expect(wrapped[0].foo).toBe(1)
  123. expect(
  124. `Set operation on key "foo" failed: target is readonly.`
  125. ).toHaveBeenWarned()
  126. // should block length mutation
  127. wrapped.length = 0
  128. expect(wrapped.length).toBe(1)
  129. expect(wrapped[0].foo).toBe(1)
  130. expect(
  131. `Set operation on key "length" failed: target is readonly.`
  132. ).toHaveBeenWarned()
  133. // mutation methods invoke set/length internally and thus are blocked as well
  134. wrapped.push(2)
  135. expect(wrapped.length).toBe(1)
  136. // push triggers two warnings on [1] and .length
  137. expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
  138. })
  139. it('should not trigger effects', () => {
  140. const wrapped: any = readonly([{ a: 1 }])
  141. let dummy
  142. effect(() => {
  143. dummy = wrapped[0].a
  144. })
  145. expect(dummy).toBe(1)
  146. wrapped[0].a = 2
  147. expect(wrapped[0].a).toBe(1)
  148. expect(dummy).toBe(1)
  149. expect(`target is readonly`).toHaveBeenWarnedTimes(1)
  150. wrapped[0] = { a: 2 }
  151. expect(wrapped[0].a).toBe(1)
  152. expect(dummy).toBe(1)
  153. expect(`target is readonly`).toHaveBeenWarnedTimes(2)
  154. })
  155. })
  156. const maps = [Map, WeakMap]
  157. maps.forEach((Collection: any) => {
  158. describe(Collection.name, () => {
  159. test('should make nested values readonly', () => {
  160. const key1 = {}
  161. const key2 = {}
  162. const original = new Collection([[key1, {}], [key2, {}]])
  163. const wrapped = readonly(original)
  164. expect(wrapped).not.toBe(original)
  165. expect(isProxy(wrapped)).toBe(true)
  166. expect(isReactive(wrapped)).toBe(false)
  167. expect(isReadonly(wrapped)).toBe(true)
  168. expect(isReactive(original)).toBe(false)
  169. expect(isReadonly(original)).toBe(false)
  170. expect(isReactive(wrapped.get(key1))).toBe(false)
  171. expect(isReadonly(wrapped.get(key1))).toBe(true)
  172. expect(isReactive(original.get(key1))).toBe(false)
  173. expect(isReadonly(original.get(key1))).toBe(false)
  174. })
  175. test('should not allow mutation & not trigger effect', () => {
  176. const map = readonly(new Collection())
  177. const key = {}
  178. let dummy
  179. effect(() => {
  180. dummy = map.get(key)
  181. })
  182. expect(dummy).toBeUndefined()
  183. map.set(key, 1)
  184. expect(dummy).toBeUndefined()
  185. expect(map.has(key)).toBe(false)
  186. expect(
  187. `Set operation on key "${key}" failed: target is readonly.`
  188. ).toHaveBeenWarned()
  189. })
  190. if (Collection === Map) {
  191. test('should retrieve readonly values on iteration', () => {
  192. const key1 = {}
  193. const key2 = {}
  194. const original = new Collection([[key1, {}], [key2, {}]])
  195. const wrapped: any = readonly(original)
  196. expect(wrapped.size).toBe(2)
  197. for (const [key, value] of wrapped) {
  198. expect(isReadonly(key)).toBe(true)
  199. expect(isReadonly(value)).toBe(true)
  200. }
  201. wrapped.forEach((value: any) => {
  202. expect(isReadonly(value)).toBe(true)
  203. })
  204. for (const value of wrapped.values()) {
  205. expect(isReadonly(value)).toBe(true)
  206. }
  207. })
  208. }
  209. })
  210. })
  211. const sets = [Set, WeakSet]
  212. sets.forEach((Collection: any) => {
  213. describe(Collection.name, () => {
  214. test('should make nested values readonly', () => {
  215. const key1 = {}
  216. const key2 = {}
  217. const original = new Collection([key1, key2])
  218. const wrapped = readonly(original)
  219. expect(wrapped).not.toBe(original)
  220. expect(isProxy(wrapped)).toBe(true)
  221. expect(isReactive(wrapped)).toBe(false)
  222. expect(isReadonly(wrapped)).toBe(true)
  223. expect(isReactive(original)).toBe(false)
  224. expect(isReadonly(original)).toBe(false)
  225. expect(wrapped.has(reactive(key1))).toBe(true)
  226. expect(original.has(reactive(key1))).toBe(false)
  227. })
  228. test('should not allow mutation & not trigger effect', () => {
  229. const set = readonly(new Collection())
  230. const key = {}
  231. let dummy
  232. effect(() => {
  233. dummy = set.has(key)
  234. })
  235. expect(dummy).toBe(false)
  236. set.add(key)
  237. expect(dummy).toBe(false)
  238. expect(set.has(key)).toBe(false)
  239. expect(
  240. `Add operation on key "${key}" failed: target is readonly.`
  241. ).toHaveBeenWarned()
  242. })
  243. if (Collection === Set) {
  244. test('should retrieve readonly values on iteration', () => {
  245. const original = new Collection([{}, {}])
  246. const wrapped: any = readonly(original)
  247. expect(wrapped.size).toBe(2)
  248. for (const value of wrapped) {
  249. expect(isReadonly(value)).toBe(true)
  250. }
  251. wrapped.forEach((value: any) => {
  252. expect(isReadonly(value)).toBe(true)
  253. })
  254. for (const value of wrapped.values()) {
  255. expect(isReadonly(value)).toBe(true)
  256. }
  257. for (const [v1, v2] of wrapped.entries()) {
  258. expect(isReadonly(v1)).toBe(true)
  259. expect(isReadonly(v2)).toBe(true)
  260. }
  261. })
  262. }
  263. })
  264. })
  265. test('calling reactive on an readonly should return readonly', () => {
  266. const a = readonly({})
  267. const b = reactive(a)
  268. expect(isReadonly(b)).toBe(true)
  269. // should point to same original
  270. expect(toRaw(a)).toBe(toRaw(b))
  271. })
  272. test('calling readonly on a reactive object should return readonly', () => {
  273. const a = reactive({})
  274. const b = readonly(a)
  275. expect(isReadonly(b)).toBe(true)
  276. // should point to same original
  277. expect(toRaw(a)).toBe(toRaw(b))
  278. })
  279. test('readonly should track and trigger if wrapping reactive original', () => {
  280. const a = reactive({ n: 1 })
  281. const b = readonly(a)
  282. // should return true since it's wrapping a reactive source
  283. expect(isReactive(b)).toBe(true)
  284. let dummy
  285. effect(() => {
  286. dummy = b.n
  287. })
  288. expect(dummy).toBe(1)
  289. a.n++
  290. expect(b.n).toBe(2)
  291. expect(dummy).toBe(2)
  292. })
  293. test('wrapping already wrapped value should return same Proxy', () => {
  294. const original = { foo: 1 }
  295. const wrapped = readonly(original)
  296. const wrapped2 = readonly(wrapped)
  297. expect(wrapped2).toBe(wrapped)
  298. })
  299. test('wrapping the same value multiple times should return same Proxy', () => {
  300. const original = { foo: 1 }
  301. const wrapped = readonly(original)
  302. const wrapped2 = readonly(original)
  303. expect(wrapped2).toBe(wrapped)
  304. })
  305. test('markRaw', () => {
  306. const obj = readonly({
  307. foo: { a: 1 },
  308. bar: markRaw({ b: 2 })
  309. })
  310. expect(isReadonly(obj.foo)).toBe(true)
  311. expect(isReactive(obj.bar)).toBe(false)
  312. })
  313. test('should make ref readonly', () => {
  314. const n: any = readonly(ref(1))
  315. n.value = 2
  316. expect(n.value).toBe(1)
  317. expect(
  318. `Set operation on key "value" failed: target is readonly.`
  319. ).toHaveBeenWarned()
  320. })
  321. describe('shallowReadonly', () => {
  322. test('should not make non-reactive properties reactive', () => {
  323. const props = shallowReadonly({ n: { foo: 1 } })
  324. expect(isReactive(props.n)).toBe(false)
  325. })
  326. test('should make root level properties readonly', () => {
  327. const props = shallowReadonly({ n: 1 })
  328. // @ts-ignore
  329. props.n = 2
  330. expect(props.n).toBe(1)
  331. expect(
  332. `Set operation on key "n" failed: target is readonly.`
  333. ).toHaveBeenWarned()
  334. })
  335. // to retain 2.x behavior.
  336. test('should NOT make nested properties readonly', () => {
  337. const props = shallowReadonly({ n: { foo: 1 } })
  338. // @ts-ignore
  339. props.n.foo = 2
  340. expect(props.n.foo).toBe(2)
  341. expect(
  342. `Set operation on key "foo" failed: target is readonly.`
  343. ).not.toHaveBeenWarned()
  344. })
  345. })
  346. })