reactiveArray.spec.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { isReactive, reactive, toRaw } from '../src/reactive'
  2. import { isRef, ref } from '../src/ref'
  3. import { effect } from '../src/effect'
  4. describe('reactivity/reactive/Array', () => {
  5. test('should make Array reactive', () => {
  6. const original = [{ foo: 1 }]
  7. const observed = reactive(original)
  8. expect(observed).not.toBe(original)
  9. expect(isReactive(observed)).toBe(true)
  10. expect(isReactive(original)).toBe(false)
  11. expect(isReactive(observed[0])).toBe(true)
  12. // get
  13. expect(observed[0].foo).toBe(1)
  14. // has
  15. expect(0 in observed).toBe(true)
  16. // ownKeys
  17. expect(Object.keys(observed)).toEqual(['0'])
  18. })
  19. test('cloned reactive Array should point to observed values', () => {
  20. const original = [{ foo: 1 }]
  21. const observed = reactive(original)
  22. const clone = observed.slice()
  23. expect(isReactive(clone[0])).toBe(true)
  24. expect(clone[0]).not.toBe(original[0])
  25. expect(clone[0]).toBe(observed[0])
  26. })
  27. test('observed value should proxy mutations to original (Array)', () => {
  28. const original: any[] = [{ foo: 1 }, { bar: 2 }]
  29. const observed = reactive(original)
  30. // set
  31. const value = { baz: 3 }
  32. const reactiveValue = reactive(value)
  33. observed[0] = value
  34. expect(observed[0]).toBe(reactiveValue)
  35. expect(original[0]).toBe(value)
  36. // delete
  37. delete observed[0]
  38. expect(observed[0]).toBeUndefined()
  39. expect(original[0]).toBeUndefined()
  40. // mutating methods
  41. observed.push(value)
  42. expect(observed[2]).toBe(reactiveValue)
  43. expect(original[2]).toBe(value)
  44. })
  45. test('Array identity methods should work with raw values', () => {
  46. const raw = {}
  47. const arr = reactive([{}, {}])
  48. arr.push(raw)
  49. expect(arr.indexOf(raw)).toBe(2)
  50. expect(arr.indexOf(raw, 3)).toBe(-1)
  51. expect(arr.includes(raw)).toBe(true)
  52. expect(arr.includes(raw, 3)).toBe(false)
  53. expect(arr.lastIndexOf(raw)).toBe(2)
  54. expect(arr.lastIndexOf(raw, 1)).toBe(-1)
  55. // should work also for the observed version
  56. const observed = arr[2]
  57. expect(arr.indexOf(observed)).toBe(2)
  58. expect(arr.indexOf(observed, 3)).toBe(-1)
  59. expect(arr.includes(observed)).toBe(true)
  60. expect(arr.includes(observed, 3)).toBe(false)
  61. expect(arr.lastIndexOf(observed)).toBe(2)
  62. expect(arr.lastIndexOf(observed, 1)).toBe(-1)
  63. })
  64. test('Array identity methods should work if raw value contains reactive objects', () => {
  65. const raw = []
  66. const obj = reactive({})
  67. raw.push(obj)
  68. const arr = reactive(raw)
  69. expect(arr.includes(obj)).toBe(true)
  70. })
  71. test('Array identity methods should be reactive', () => {
  72. const obj = {}
  73. const arr = reactive([obj, {}])
  74. let index: number = -1
  75. effect(() => {
  76. index = arr.indexOf(obj)
  77. })
  78. expect(index).toBe(0)
  79. arr.reverse()
  80. expect(index).toBe(1)
  81. })
  82. test('delete on Array should not trigger length dependency', () => {
  83. const arr = reactive([1, 2, 3])
  84. const fn = vi.fn()
  85. effect(() => {
  86. fn(arr.length)
  87. })
  88. expect(fn).toHaveBeenCalledTimes(1)
  89. delete arr[1]
  90. expect(fn).toHaveBeenCalledTimes(1)
  91. })
  92. test('should track hasOwnProperty call with index', () => {
  93. const original = [1, 2, 3]
  94. const observed = reactive(original)
  95. let dummy
  96. effect(() => {
  97. dummy = observed.hasOwnProperty(0)
  98. })
  99. expect(dummy).toBe(true)
  100. delete observed[0]
  101. expect(dummy).toBe(false)
  102. })
  103. test('shift on Array should trigger dependency once', () => {
  104. const arr = reactive([1, 2, 3])
  105. const fn = vi.fn()
  106. effect(() => {
  107. for (let i = 0; i < arr.length; i++) {
  108. arr[i]
  109. }
  110. fn()
  111. })
  112. expect(fn).toHaveBeenCalledTimes(1)
  113. arr.shift()
  114. expect(fn).toHaveBeenCalledTimes(2)
  115. })
  116. //#6018
  117. test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
  118. const arr = ref([1])
  119. const fn1 = vi.fn()
  120. const fn2 = vi.fn()
  121. effect(() => {
  122. fn1()
  123. if (arr.value.length > 0) {
  124. arr.value.slice()
  125. fn2()
  126. }
  127. })
  128. expect(fn1).toHaveBeenCalledTimes(1)
  129. expect(fn2).toHaveBeenCalledTimes(1)
  130. arr.value.splice(0)
  131. expect(fn1).toHaveBeenCalledTimes(2)
  132. expect(fn2).toHaveBeenCalledTimes(1)
  133. })
  134. test('add existing index on Array should not trigger length dependency', () => {
  135. const array = new Array(3)
  136. const observed = reactive(array)
  137. const fn = vi.fn()
  138. effect(() => {
  139. fn(observed.length)
  140. })
  141. expect(fn).toHaveBeenCalledTimes(1)
  142. observed[1] = 1
  143. expect(fn).toHaveBeenCalledTimes(1)
  144. })
  145. test('add non-integer prop on Array should not trigger length dependency', () => {
  146. const array: any[] & { x?: string } = new Array(3)
  147. const observed = reactive(array)
  148. const fn = vi.fn()
  149. effect(() => {
  150. fn(observed.length)
  151. })
  152. expect(fn).toHaveBeenCalledTimes(1)
  153. observed.x = 'x'
  154. expect(fn).toHaveBeenCalledTimes(1)
  155. observed[-1] = 'x'
  156. expect(fn).toHaveBeenCalledTimes(1)
  157. observed[NaN] = 'x'
  158. expect(fn).toHaveBeenCalledTimes(1)
  159. })
  160. // #2427
  161. test('track length on for ... in iteration', () => {
  162. const array = reactive([1])
  163. let length = ''
  164. effect(() => {
  165. length = ''
  166. for (const key in array) {
  167. length += key
  168. }
  169. })
  170. expect(length).toBe('0')
  171. array.push(1)
  172. expect(length).toBe('01')
  173. })
  174. // #9742
  175. test('mutation on user proxy of reactive Array', () => {
  176. const array = reactive<number[]>([])
  177. const proxy = new Proxy(array, {})
  178. proxy.push(1)
  179. expect(array).toHaveLength(1)
  180. expect(proxy).toHaveLength(1)
  181. })
  182. describe('Array methods w/ refs', () => {
  183. let original: any[]
  184. beforeEach(() => {
  185. original = reactive([1, ref(2)])
  186. })
  187. // read + copy
  188. test('read only copy methods', () => {
  189. const raw = original.concat([3, ref(4)])
  190. expect(isRef(raw[1])).toBe(true)
  191. expect(isRef(raw[3])).toBe(true)
  192. })
  193. // read + write
  194. test('read + write mutating methods', () => {
  195. const res = original.copyWithin(0, 1, 2)
  196. const raw = toRaw(res)
  197. expect(isRef(raw[0])).toBe(true)
  198. expect(isRef(raw[1])).toBe(true)
  199. })
  200. test('read + identity', () => {
  201. const ref = original[1]
  202. expect(ref).toBe(toRaw(original)[1])
  203. expect(original.indexOf(ref)).toBe(1)
  204. })
  205. })
  206. describe('Array subclasses', () => {
  207. class SubArray<T> extends Array<T> {
  208. lastPushed: undefined | T
  209. lastSearched: undefined | T
  210. push(item: T) {
  211. this.lastPushed = item
  212. return super.push(item)
  213. }
  214. indexOf(searchElement: T, fromIndex?: number | undefined): number {
  215. this.lastSearched = searchElement
  216. return super.indexOf(searchElement, fromIndex)
  217. }
  218. }
  219. test('calls correct mutation method on Array subclass', () => {
  220. const subArray = new SubArray(4, 5, 6)
  221. const observed = reactive(subArray)
  222. subArray.push(7)
  223. expect(subArray.lastPushed).toBe(7)
  224. observed.push(9)
  225. expect(observed.lastPushed).toBe(9)
  226. })
  227. test('calls correct identity-sensitive method on Array subclass', () => {
  228. const subArray = new SubArray(4, 5, 6)
  229. const observed = reactive(subArray)
  230. let index
  231. index = subArray.indexOf(4)
  232. expect(index).toBe(0)
  233. expect(subArray.lastSearched).toBe(4)
  234. index = observed.indexOf(6)
  235. expect(index).toBe(2)
  236. expect(observed.lastSearched).toBe(6)
  237. })
  238. })
  239. })