readonly.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import {
  2. reactive,
  3. readonly,
  4. toRaw,
  5. isReactive,
  6. isReadonly,
  7. markNonReactive,
  8. markReadonly,
  9. lock,
  10. unlock,
  11. effect,
  12. ref,
  13. shallowReadonly
  14. } from '../src'
  15. import { mockWarn } from '@vue/runtime-test'
  16. /**
  17. * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html
  18. */
  19. type Writable<T> = { -readonly [P in keyof T]: T[P] }
  20. describe('reactivity/readonly', () => {
  21. mockWarn()
  22. describe('Object', () => {
  23. it('should make nested values readonly', () => {
  24. const original = { foo: 1, bar: { baz: 2 } }
  25. const observed = readonly(original)
  26. expect(observed).not.toBe(original)
  27. expect(isReactive(observed)).toBe(true)
  28. expect(isReadonly(observed)).toBe(true)
  29. expect(isReactive(original)).toBe(false)
  30. expect(isReadonly(original)).toBe(false)
  31. expect(isReactive(observed.bar)).toBe(true)
  32. expect(isReadonly(observed.bar)).toBe(true)
  33. expect(isReactive(original.bar)).toBe(false)
  34. expect(isReadonly(original.bar)).toBe(false)
  35. // get
  36. expect(observed.foo).toBe(1)
  37. // has
  38. expect('foo' in observed).toBe(true)
  39. // ownKeys
  40. expect(Object.keys(observed)).toEqual(['foo', 'bar'])
  41. })
  42. it('should not allow mutation', () => {
  43. const qux = Symbol('qux')
  44. const original = {
  45. foo: 1,
  46. bar: {
  47. baz: 2
  48. },
  49. [qux]: 3
  50. }
  51. const observed: Writable<typeof original> = readonly(original)
  52. observed.foo = 2
  53. expect(observed.foo).toBe(1)
  54. expect(
  55. `Set operation on key "foo" failed: target is readonly.`
  56. ).toHaveBeenWarnedLast()
  57. observed.bar.baz = 3
  58. expect(observed.bar.baz).toBe(2)
  59. expect(
  60. `Set operation on key "baz" failed: target is readonly.`
  61. ).toHaveBeenWarnedLast()
  62. observed[qux] = 4
  63. expect(observed[qux]).toBe(3)
  64. expect(
  65. `Set operation on key "Symbol(qux)" failed: target is readonly.`
  66. ).toHaveBeenWarnedLast()
  67. delete observed.foo
  68. expect(observed.foo).toBe(1)
  69. expect(
  70. `Delete operation on key "foo" failed: target is readonly.`
  71. ).toHaveBeenWarnedLast()
  72. delete observed.bar.baz
  73. expect(observed.bar.baz).toBe(2)
  74. expect(
  75. `Delete operation on key "baz" failed: target is readonly.`
  76. ).toHaveBeenWarnedLast()
  77. delete observed[qux]
  78. expect(observed[qux]).toBe(3)
  79. expect(
  80. `Delete operation on key "Symbol(qux)" failed: target is readonly.`
  81. ).toHaveBeenWarnedLast()
  82. })
  83. it('should allow mutation when unlocked', () => {
  84. const observed: any = readonly({ foo: 1, bar: { baz: 2 } })
  85. unlock()
  86. observed.prop = 2
  87. observed.bar.qux = 3
  88. delete observed.bar.baz
  89. delete observed.foo
  90. lock()
  91. expect(observed.prop).toBe(2)
  92. expect(observed.foo).toBeUndefined()
  93. expect(observed.bar.qux).toBe(3)
  94. expect('baz' in observed.bar).toBe(false)
  95. expect(`target is readonly`).not.toHaveBeenWarned()
  96. })
  97. it('should not trigger effects when locked', () => {
  98. const observed: any = readonly({ a: 1 })
  99. let dummy
  100. effect(() => {
  101. dummy = observed.a
  102. })
  103. expect(dummy).toBe(1)
  104. observed.a = 2
  105. expect(observed.a).toBe(1)
  106. expect(dummy).toBe(1)
  107. expect(`target is readonly`).toHaveBeenWarned()
  108. })
  109. it('should trigger effects when unlocked', () => {
  110. const observed: any = readonly({ a: 1 })
  111. let dummy
  112. effect(() => {
  113. dummy = observed.a
  114. })
  115. expect(dummy).toBe(1)
  116. unlock()
  117. observed.a = 2
  118. lock()
  119. expect(observed.a).toBe(2)
  120. expect(dummy).toBe(2)
  121. })
  122. })
  123. describe('Array', () => {
  124. it('should make nested values readonly', () => {
  125. const original = [{ foo: 1 }]
  126. const observed = readonly(original)
  127. expect(observed).not.toBe(original)
  128. expect(isReactive(observed)).toBe(true)
  129. expect(isReadonly(observed)).toBe(true)
  130. expect(isReactive(original)).toBe(false)
  131. expect(isReadonly(original)).toBe(false)
  132. expect(isReactive(observed[0])).toBe(true)
  133. expect(isReadonly(observed[0])).toBe(true)
  134. expect(isReactive(original[0])).toBe(false)
  135. expect(isReadonly(original[0])).toBe(false)
  136. // get
  137. expect(observed[0].foo).toBe(1)
  138. // has
  139. expect(0 in observed).toBe(true)
  140. // ownKeys
  141. expect(Object.keys(observed)).toEqual(['0'])
  142. })
  143. it('should not allow mutation', () => {
  144. const observed: any = readonly([{ foo: 1 }])
  145. observed[0] = 1
  146. expect(observed[0]).not.toBe(1)
  147. expect(
  148. `Set operation on key "0" failed: target is readonly.`
  149. ).toHaveBeenWarned()
  150. observed[0].foo = 2
  151. expect(observed[0].foo).toBe(1)
  152. expect(
  153. `Set operation on key "foo" failed: target is readonly.`
  154. ).toHaveBeenWarned()
  155. // should block length mutation
  156. observed.length = 0
  157. expect(observed.length).toBe(1)
  158. expect(observed[0].foo).toBe(1)
  159. expect(
  160. `Set operation on key "length" failed: target is readonly.`
  161. ).toHaveBeenWarned()
  162. // mutation methods invoke set/length internally and thus are blocked as well
  163. observed.push(2)
  164. expect(observed.length).toBe(1)
  165. // push triggers two warnings on [1] and .length
  166. expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
  167. })
  168. it('should allow mutation when unlocked', () => {
  169. const observed: any = readonly([{ foo: 1, bar: { baz: 2 } }])
  170. unlock()
  171. observed[1] = 2
  172. observed.push(3)
  173. observed[0].foo = 2
  174. observed[0].bar.baz = 3
  175. lock()
  176. expect(observed.length).toBe(3)
  177. expect(observed[1]).toBe(2)
  178. expect(observed[2]).toBe(3)
  179. expect(observed[0].foo).toBe(2)
  180. expect(observed[0].bar.baz).toBe(3)
  181. expect(`target is readonly`).not.toHaveBeenWarned()
  182. })
  183. it('should not trigger effects when locked', () => {
  184. const observed: any = readonly([{ a: 1 }])
  185. let dummy
  186. effect(() => {
  187. dummy = observed[0].a
  188. })
  189. expect(dummy).toBe(1)
  190. observed[0].a = 2
  191. expect(observed[0].a).toBe(1)
  192. expect(dummy).toBe(1)
  193. expect(`target is readonly`).toHaveBeenWarnedTimes(1)
  194. observed[0] = { a: 2 }
  195. expect(observed[0].a).toBe(1)
  196. expect(dummy).toBe(1)
  197. expect(`target is readonly`).toHaveBeenWarnedTimes(2)
  198. })
  199. it('should trigger effects when unlocked', () => {
  200. const observed: any = readonly([{ a: 1 }])
  201. let dummy
  202. effect(() => {
  203. dummy = observed[0].a
  204. })
  205. expect(dummy).toBe(1)
  206. unlock()
  207. observed[0].a = 2
  208. expect(observed[0].a).toBe(2)
  209. expect(dummy).toBe(2)
  210. observed[0] = { a: 3 }
  211. expect(observed[0].a).toBe(3)
  212. expect(dummy).toBe(3)
  213. observed.unshift({ a: 4 })
  214. expect(observed[0].a).toBe(4)
  215. expect(dummy).toBe(4)
  216. lock()
  217. })
  218. })
  219. const maps = [Map, WeakMap]
  220. maps.forEach((Collection: any) => {
  221. describe(Collection.name, () => {
  222. test('should make nested values readonly', () => {
  223. const key1 = {}
  224. const key2 = {}
  225. const original = new Collection([[key1, {}], [key2, {}]])
  226. const observed = readonly(original)
  227. expect(observed).not.toBe(original)
  228. expect(isReactive(observed)).toBe(true)
  229. expect(isReadonly(observed)).toBe(true)
  230. expect(isReactive(original)).toBe(false)
  231. expect(isReadonly(original)).toBe(false)
  232. expect(isReactive(observed.get(key1))).toBe(true)
  233. expect(isReadonly(observed.get(key1))).toBe(true)
  234. expect(isReactive(original.get(key1))).toBe(false)
  235. expect(isReadonly(original.get(key1))).toBe(false)
  236. })
  237. test('should not allow mutation & not trigger effect', () => {
  238. const map = readonly(new Collection())
  239. const key = {}
  240. let dummy
  241. effect(() => {
  242. dummy = map.get(key)
  243. })
  244. expect(dummy).toBeUndefined()
  245. map.set(key, 1)
  246. expect(dummy).toBeUndefined()
  247. expect(map.has(key)).toBe(false)
  248. expect(
  249. `Set operation on key "${key}" failed: target is readonly.`
  250. ).toHaveBeenWarned()
  251. })
  252. test('should allow mutation & trigger effect when unlocked', () => {
  253. const map = readonly(new Collection())
  254. const isWeak = Collection === WeakMap
  255. const key = {}
  256. let dummy
  257. effect(() => {
  258. dummy = map.get(key) + (isWeak ? 0 : map.size)
  259. })
  260. expect(dummy).toBeNaN()
  261. unlock()
  262. map.set(key, 1)
  263. lock()
  264. expect(dummy).toBe(isWeak ? 1 : 2)
  265. expect(map.get(key)).toBe(1)
  266. expect(`target is readonly`).not.toHaveBeenWarned()
  267. })
  268. if (Collection === Map) {
  269. test('should retrieve readonly values on iteration', () => {
  270. const key1 = {}
  271. const key2 = {}
  272. const original = new Collection([[key1, {}], [key2, {}]])
  273. const observed: any = readonly(original)
  274. for (const [key, value] of observed) {
  275. expect(isReadonly(key)).toBe(true)
  276. expect(isReadonly(value)).toBe(true)
  277. }
  278. observed.forEach((value: any) => {
  279. expect(isReadonly(value)).toBe(true)
  280. })
  281. for (const value of observed.values()) {
  282. expect(isReadonly(value)).toBe(true)
  283. }
  284. })
  285. }
  286. })
  287. })
  288. const sets = [Set, WeakSet]
  289. sets.forEach((Collection: any) => {
  290. describe(Collection.name, () => {
  291. test('should make nested values readonly', () => {
  292. const key1 = {}
  293. const key2 = {}
  294. const original = new Collection([key1, key2])
  295. const observed = readonly(original)
  296. expect(observed).not.toBe(original)
  297. expect(isReactive(observed)).toBe(true)
  298. expect(isReadonly(observed)).toBe(true)
  299. expect(isReactive(original)).toBe(false)
  300. expect(isReadonly(original)).toBe(false)
  301. expect(observed.has(reactive(key1))).toBe(true)
  302. expect(original.has(reactive(key1))).toBe(false)
  303. })
  304. test('should not allow mutation & not trigger effect', () => {
  305. const set = readonly(new Collection())
  306. const key = {}
  307. let dummy
  308. effect(() => {
  309. dummy = set.has(key)
  310. })
  311. expect(dummy).toBe(false)
  312. set.add(key)
  313. expect(dummy).toBe(false)
  314. expect(set.has(key)).toBe(false)
  315. expect(
  316. `Add operation on key "${key}" failed: target is readonly.`
  317. ).toHaveBeenWarned()
  318. })
  319. test('should allow mutation & trigger effect when unlocked', () => {
  320. const set = readonly(new Collection())
  321. const key = {}
  322. let dummy
  323. effect(() => {
  324. dummy = set.has(key)
  325. })
  326. expect(dummy).toBe(false)
  327. unlock()
  328. set.add(key)
  329. lock()
  330. expect(dummy).toBe(true)
  331. expect(set.has(key)).toBe(true)
  332. expect(`target is readonly`).not.toHaveBeenWarned()
  333. })
  334. if (Collection === Set) {
  335. test('should retrieve readonly values on iteration', () => {
  336. const original = new Collection([{}, {}])
  337. const observed: any = readonly(original)
  338. for (const value of observed) {
  339. expect(isReadonly(value)).toBe(true)
  340. }
  341. observed.forEach((value: any) => {
  342. expect(isReadonly(value)).toBe(true)
  343. })
  344. for (const value of observed.values()) {
  345. expect(isReadonly(value)).toBe(true)
  346. }
  347. for (const [v1, v2] of observed.entries()) {
  348. expect(isReadonly(v1)).toBe(true)
  349. expect(isReadonly(v2)).toBe(true)
  350. }
  351. })
  352. }
  353. })
  354. })
  355. test('calling reactive on an readonly should return readonly', () => {
  356. const a = readonly({})
  357. const b = reactive(a)
  358. expect(isReadonly(b)).toBe(true)
  359. // should point to same original
  360. expect(toRaw(a)).toBe(toRaw(b))
  361. })
  362. test('calling readonly on a reactive object should return readonly', () => {
  363. const a = reactive({})
  364. const b = readonly(a)
  365. expect(isReadonly(b)).toBe(true)
  366. // should point to same original
  367. expect(toRaw(a)).toBe(toRaw(b))
  368. })
  369. test('observing already observed value should return same Proxy', () => {
  370. const original = { foo: 1 }
  371. const observed = readonly(original)
  372. const observed2 = readonly(observed)
  373. expect(observed2).toBe(observed)
  374. })
  375. test('observing the same value multiple times should return same Proxy', () => {
  376. const original = { foo: 1 }
  377. const observed = readonly(original)
  378. const observed2 = readonly(original)
  379. expect(observed2).toBe(observed)
  380. })
  381. test('markNonReactive', () => {
  382. const obj = readonly({
  383. foo: { a: 1 },
  384. bar: markNonReactive({ b: 2 })
  385. })
  386. expect(isReactive(obj.foo)).toBe(true)
  387. expect(isReactive(obj.bar)).toBe(false)
  388. })
  389. test('markReadonly', () => {
  390. const obj = reactive({
  391. foo: { a: 1 },
  392. bar: markReadonly({ b: 2 })
  393. })
  394. expect(isReactive(obj.foo)).toBe(true)
  395. expect(isReactive(obj.bar)).toBe(true)
  396. expect(isReadonly(obj.foo)).toBe(false)
  397. expect(isReadonly(obj.bar)).toBe(true)
  398. })
  399. test('should make ref readonly', () => {
  400. const n: any = readonly(ref(1))
  401. n.value = 2
  402. expect(n.value).toBe(1)
  403. expect(
  404. `Set operation on key "value" failed: target is readonly.`
  405. ).toHaveBeenWarned()
  406. })
  407. describe('shallowReadonly', () => {
  408. test('should not make non-reactive properties reactive', () => {
  409. const props = shallowReadonly({ n: { foo: 1 } })
  410. expect(isReactive(props.n)).toBe(false)
  411. })
  412. test('should make root level properties readonly', () => {
  413. const props = shallowReadonly({ n: 1 })
  414. // @ts-ignore
  415. props.n = 2
  416. expect(props.n).toBe(1)
  417. expect(
  418. `Set operation on key "n" failed: target is readonly.`
  419. ).toHaveBeenWarned()
  420. })
  421. // to retain 2.x behavior.
  422. test('should NOT make nested properties readonly', () => {
  423. const props = shallowReadonly({ n: { foo: 1 } })
  424. // @ts-ignore
  425. props.n.foo = 2
  426. expect(props.n.foo).toBe(2)
  427. expect(
  428. `Set operation on key "foo" failed: target is readonly.`
  429. ).not.toHaveBeenWarned()
  430. })
  431. })
  432. })