reactiveArray.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. import { type ComputedRef, computed } from '../src/computed'
  2. import { isReactive, reactive, shallowReactive, toRaw } from '../src/reactive'
  3. import { isRef, ref } from '../src/ref'
  4. import { effect } from '../src/effect'
  5. describe('reactivity/reactive/Array', () => {
  6. test('should make Array reactive', () => {
  7. const original = [{ foo: 1 }]
  8. const observed = reactive(original)
  9. expect(observed).not.toBe(original)
  10. expect(isReactive(observed)).toBe(true)
  11. expect(isReactive(original)).toBe(false)
  12. expect(isReactive(observed[0])).toBe(true)
  13. // get
  14. expect(observed[0].foo).toBe(1)
  15. // has
  16. expect(0 in observed).toBe(true)
  17. // ownKeys
  18. expect(Object.keys(observed)).toEqual(['0'])
  19. })
  20. test('cloned reactive Array should point to observed values', () => {
  21. const original = [{ foo: 1 }]
  22. const observed = reactive(original)
  23. const clone = observed.slice()
  24. expect(isReactive(clone[0])).toBe(true)
  25. expect(clone[0]).not.toBe(original[0])
  26. expect(clone[0]).toBe(observed[0])
  27. })
  28. test('observed value should proxy mutations to original (Array)', () => {
  29. const original: any[] = [{ foo: 1 }, { bar: 2 }]
  30. const observed = reactive(original)
  31. // set
  32. const value = { baz: 3 }
  33. const reactiveValue = reactive(value)
  34. observed[0] = value
  35. expect(observed[0]).toBe(reactiveValue)
  36. expect(original[0]).toBe(value)
  37. // delete
  38. delete observed[0]
  39. expect(observed[0]).toBeUndefined()
  40. expect(original[0]).toBeUndefined()
  41. // mutating methods
  42. observed.push(value)
  43. expect(observed[2]).toBe(reactiveValue)
  44. expect(original[2]).toBe(value)
  45. })
  46. test('Array identity methods should work with raw values', () => {
  47. const raw = {}
  48. const arr = reactive([{}, {}])
  49. arr.push(raw)
  50. expect(arr.indexOf(raw)).toBe(2)
  51. expect(arr.indexOf(raw, 3)).toBe(-1)
  52. expect(arr.includes(raw)).toBe(true)
  53. expect(arr.includes(raw, 3)).toBe(false)
  54. expect(arr.lastIndexOf(raw)).toBe(2)
  55. expect(arr.lastIndexOf(raw, 1)).toBe(-1)
  56. // should work also for the observed version
  57. const observed = arr[2]
  58. expect(arr.indexOf(observed)).toBe(2)
  59. expect(arr.indexOf(observed, 3)).toBe(-1)
  60. expect(arr.includes(observed)).toBe(true)
  61. expect(arr.includes(observed, 3)).toBe(false)
  62. expect(arr.lastIndexOf(observed)).toBe(2)
  63. expect(arr.lastIndexOf(observed, 1)).toBe(-1)
  64. })
  65. test('Array identity methods should work if raw value contains reactive objects', () => {
  66. const raw = []
  67. const obj = reactive({})
  68. raw.push(obj)
  69. const arr = reactive(raw)
  70. expect(arr.includes(obj)).toBe(true)
  71. })
  72. test('Array identity methods should be reactive', () => {
  73. const obj = {}
  74. const arr = reactive([obj, {}])
  75. let index: number = -1
  76. effect(() => {
  77. index = arr.indexOf(obj)
  78. })
  79. expect(index).toBe(0)
  80. arr.reverse()
  81. expect(index).toBe(1)
  82. })
  83. test('delete on Array should not trigger length dependency', () => {
  84. const arr = reactive([1, 2, 3])
  85. const fn = vi.fn()
  86. effect(() => {
  87. fn(arr.length)
  88. })
  89. expect(fn).toHaveBeenCalledTimes(1)
  90. delete arr[1]
  91. expect(fn).toHaveBeenCalledTimes(1)
  92. })
  93. test('shift on Array should trigger dependency once', () => {
  94. const arr = reactive([1, 2, 3])
  95. const fn = vi.fn()
  96. effect(() => {
  97. for (let i = 0; i < arr.length; i++) {
  98. arr[i]
  99. }
  100. fn()
  101. })
  102. expect(fn).toHaveBeenCalledTimes(1)
  103. arr.shift()
  104. expect(fn).toHaveBeenCalledTimes(2)
  105. })
  106. //#6018
  107. test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
  108. const arr = ref([1])
  109. const fn1 = vi.fn()
  110. const fn2 = vi.fn()
  111. effect(() => {
  112. fn1()
  113. if (arr.value.length > 0) {
  114. arr.value.slice()
  115. fn2()
  116. }
  117. })
  118. expect(fn1).toHaveBeenCalledTimes(1)
  119. expect(fn2).toHaveBeenCalledTimes(1)
  120. arr.value.splice(0)
  121. expect(fn1).toHaveBeenCalledTimes(2)
  122. expect(fn2).toHaveBeenCalledTimes(1)
  123. })
  124. test('add existing index on Array should not trigger length dependency', () => {
  125. const array = new Array(3)
  126. const observed = reactive(array)
  127. const fn = vi.fn()
  128. effect(() => {
  129. fn(observed.length)
  130. })
  131. expect(fn).toHaveBeenCalledTimes(1)
  132. observed[1] = 1
  133. expect(fn).toHaveBeenCalledTimes(1)
  134. })
  135. test('add non-integer prop on Array should not trigger length dependency', () => {
  136. const array: any[] & { x?: string } = new Array(3)
  137. const observed = reactive(array)
  138. const fn = vi.fn()
  139. effect(() => {
  140. fn(observed.length)
  141. })
  142. expect(fn).toHaveBeenCalledTimes(1)
  143. observed.x = 'x'
  144. expect(fn).toHaveBeenCalledTimes(1)
  145. observed[-1] = 'x'
  146. expect(fn).toHaveBeenCalledTimes(1)
  147. observed[NaN] = 'x'
  148. expect(fn).toHaveBeenCalledTimes(1)
  149. })
  150. // #2427
  151. test('track length on for ... in iteration', () => {
  152. const array = reactive([1])
  153. let length = ''
  154. effect(() => {
  155. length = ''
  156. for (const key in array) {
  157. length += key
  158. }
  159. })
  160. expect(length).toBe('0')
  161. array.push(1)
  162. expect(length).toBe('01')
  163. })
  164. // #9742
  165. test('mutation on user proxy of reactive Array', () => {
  166. const array = reactive<number[]>([])
  167. const proxy = new Proxy(array, {})
  168. proxy.push(1)
  169. expect(array).toHaveLength(1)
  170. expect(proxy).toHaveLength(1)
  171. })
  172. describe('Array methods w/ refs', () => {
  173. let original: any[]
  174. beforeEach(() => {
  175. original = reactive([1, ref(2)])
  176. })
  177. // read + copy
  178. test('read only copy methods', () => {
  179. const raw = original.concat([3, ref(4)])
  180. expect(isRef(raw[1])).toBe(true)
  181. expect(isRef(raw[3])).toBe(true)
  182. })
  183. // read + write
  184. test('read + write mutating methods', () => {
  185. const res = original.copyWithin(0, 1, 2)
  186. const raw = toRaw(res)
  187. expect(isRef(raw[0])).toBe(true)
  188. expect(isRef(raw[1])).toBe(true)
  189. })
  190. test('read + identity', () => {
  191. const ref = original[1]
  192. expect(ref).toBe(toRaw(original)[1])
  193. expect(original.indexOf(ref)).toBe(1)
  194. })
  195. })
  196. describe('Array subclasses', () => {
  197. class SubArray<T> extends Array<T> {
  198. lastPushed: undefined | T
  199. lastSearched: undefined | T
  200. push(item: T) {
  201. this.lastPushed = item
  202. return super.push(item)
  203. }
  204. indexOf(searchElement: T, fromIndex?: number | undefined): number {
  205. this.lastSearched = searchElement
  206. return super.indexOf(searchElement, fromIndex)
  207. }
  208. }
  209. test('calls correct mutation method on Array subclass', () => {
  210. const subArray = new SubArray(4, 5, 6)
  211. const observed = reactive(subArray)
  212. subArray.push(7)
  213. expect(subArray.lastPushed).toBe(7)
  214. observed.push(9)
  215. expect(observed.lastPushed).toBe(9)
  216. })
  217. test('calls correct identity-sensitive method on Array subclass', () => {
  218. const subArray = new SubArray(4, 5, 6)
  219. const observed = reactive(subArray)
  220. let index
  221. index = subArray.indexOf(4)
  222. expect(index).toBe(0)
  223. expect(subArray.lastSearched).toBe(4)
  224. index = observed.indexOf(6)
  225. expect(index).toBe(2)
  226. expect(observed.lastSearched).toBe(6)
  227. })
  228. })
  229. describe('Optimized array methods:', () => {
  230. test('iterator', () => {
  231. const shallow = shallowReactive([1, 2, 3, 4])
  232. let result = computed(() => {
  233. let sum = 0
  234. for (let x of shallow) {
  235. sum += x ** 2
  236. }
  237. return sum
  238. })
  239. expect(result.value).toBe(30)
  240. shallow[2] = 0
  241. expect(result.value).toBe(21)
  242. const deep = reactive([{ val: 1 }, { val: 2 }])
  243. result = computed(() => {
  244. let sum = 0
  245. for (let x of deep) {
  246. sum += x.val ** 2
  247. }
  248. return sum
  249. })
  250. expect(result.value).toBe(5)
  251. deep[1].val = 3
  252. expect(result.value).toBe(10)
  253. })
  254. test('concat', () => {
  255. const a1 = shallowReactive([1, { val: 2 }])
  256. const a2 = reactive([{ val: 3 }])
  257. const a3 = [4, 5]
  258. let result = computed(() => a1.concat(a2, a3))
  259. expect(result.value).toStrictEqual([1, { val: 2 }, { val: 3 }, 4, 5])
  260. expect(isReactive(result.value[1])).toBe(false)
  261. expect(isReactive(result.value[2])).toBe(true)
  262. a1.shift()
  263. expect(result.value).toStrictEqual([{ val: 2 }, { val: 3 }, 4, 5])
  264. a2.pop()
  265. expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
  266. a3.pop()
  267. expect(result.value).toStrictEqual([{ val: 2 }, 4, 5])
  268. })
  269. test('entries', () => {
  270. const shallow = shallowReactive([0, 1])
  271. const result1 = computed(() => Array.from(shallow.entries()))
  272. expect(result1.value).toStrictEqual([
  273. [0, 0],
  274. [1, 1],
  275. ])
  276. shallow[1] = 10
  277. expect(result1.value).toStrictEqual([
  278. [0, 0],
  279. [1, 10],
  280. ])
  281. const deep = reactive([{ val: 0 }, { val: 1 }])
  282. const result2 = computed(() => Array.from(deep.entries()))
  283. expect(result2.value).toStrictEqual([
  284. [0, { val: 0 }],
  285. [1, { val: 1 }],
  286. ])
  287. expect(isReactive(result2.value[0][1])).toBe(true)
  288. deep.pop()
  289. expect(Array.from(result2.value)).toStrictEqual([[0, { val: 0 }]])
  290. })
  291. test('every', () => {
  292. const shallow = shallowReactive([1, 2, 5])
  293. let result = computed(() => shallow.every(x => x < 5))
  294. expect(result.value).toBe(false)
  295. shallow.pop()
  296. expect(result.value).toBe(true)
  297. const deep = reactive([{ val: 1 }, { val: 5 }])
  298. result = computed(() => deep.every(x => x.val < 5))
  299. expect(result.value).toBe(false)
  300. deep[1].val = 2
  301. expect(result.value).toBe(true)
  302. })
  303. test('filter', () => {
  304. const shallow = shallowReactive([1, 2, 3, 4])
  305. const result1 = computed(() => shallow.filter(x => x < 3))
  306. expect(result1.value).toStrictEqual([1, 2])
  307. shallow[2] = 0
  308. expect(result1.value).toStrictEqual([1, 2, 0])
  309. const deep = reactive([{ val: 1 }, { val: 2 }])
  310. const result2 = computed(() => deep.filter(x => x.val < 2))
  311. expect(result2.value).toStrictEqual([{ val: 1 }])
  312. expect(isReactive(result2.value[0])).toBe(true)
  313. deep[1].val = 0
  314. expect(result2.value).toStrictEqual([{ val: 1 }, { val: 0 }])
  315. })
  316. test('find and co.', () => {
  317. const shallow = shallowReactive([{ val: 1 }, { val: 2 }])
  318. let find = computed(() => shallow.find(x => x.val === 2))
  319. // @ts-expect-error tests are not limited to es2016
  320. let findLast = computed(() => shallow.findLast(x => x.val === 2))
  321. let findIndex = computed(() => shallow.findIndex(x => x.val === 2))
  322. let findLastIndex = computed(() =>
  323. // @ts-expect-error tests are not limited to es2016
  324. shallow.findLastIndex(x => x.val === 2),
  325. )
  326. expect(find.value).toBe(shallow[1])
  327. expect(isReactive(find.value)).toBe(false)
  328. expect(findLast.value).toBe(shallow[1])
  329. expect(isReactive(findLast.value)).toBe(false)
  330. expect(findIndex.value).toBe(1)
  331. expect(findLastIndex.value).toBe(1)
  332. shallow[1].val = 0
  333. expect(find.value).toBe(shallow[1])
  334. expect(findLast.value).toBe(shallow[1])
  335. expect(findIndex.value).toBe(1)
  336. expect(findLastIndex.value).toBe(1)
  337. shallow.pop()
  338. expect(find.value).toBe(undefined)
  339. expect(findLast.value).toBe(undefined)
  340. expect(findIndex.value).toBe(-1)
  341. expect(findLastIndex.value).toBe(-1)
  342. const deep = reactive([{ val: 1 }, { val: 2 }])
  343. find = computed(() => deep.find(x => x.val === 2))
  344. // @ts-expect-error tests are not limited to es2016
  345. findLast = computed(() => deep.findLast(x => x.val === 2))
  346. findIndex = computed(() => deep.findIndex(x => x.val === 2))
  347. // @ts-expect-error tests are not limited to es2016
  348. findLastIndex = computed(() => deep.findLastIndex(x => x.val === 2))
  349. expect(find.value).toBe(deep[1])
  350. expect(isReactive(find.value)).toBe(true)
  351. expect(findLast.value).toBe(deep[1])
  352. expect(isReactive(findLast.value)).toBe(true)
  353. expect(findIndex.value).toBe(1)
  354. expect(findLastIndex.value).toBe(1)
  355. deep[1].val = 0
  356. expect(find.value).toBe(undefined)
  357. expect(findLast.value).toBe(undefined)
  358. expect(findIndex.value).toBe(-1)
  359. expect(findLastIndex.value).toBe(-1)
  360. })
  361. test('forEach', () => {
  362. const shallow = shallowReactive([1, 2, 3, 4])
  363. let result = computed(() => {
  364. let sum = 0
  365. shallow.forEach(x => (sum += x ** 2))
  366. return sum
  367. })
  368. expect(result.value).toBe(30)
  369. shallow[2] = 0
  370. expect(result.value).toBe(21)
  371. const deep = reactive([{ val: 1 }, { val: 2 }])
  372. result = computed(() => {
  373. let sum = 0
  374. deep.forEach(x => (sum += x.val ** 2))
  375. return sum
  376. })
  377. expect(result.value).toBe(5)
  378. deep[1].val = 3
  379. expect(result.value).toBe(10)
  380. })
  381. test('join', () => {
  382. function toString(this: { val: number }) {
  383. return this.val
  384. }
  385. const shallow = shallowReactive([
  386. { val: 1, toString },
  387. { val: 2, toString },
  388. ])
  389. let result = computed(() => shallow.join('+'))
  390. expect(result.value).toBe('1+2')
  391. shallow[1].val = 23
  392. expect(result.value).toBe('1+2')
  393. shallow.pop()
  394. expect(result.value).toBe('1')
  395. const deep = reactive([
  396. { val: 1, toString },
  397. { val: 2, toString },
  398. ])
  399. result = computed(() => deep.join())
  400. expect(result.value).toBe('1,2')
  401. deep[1].val = 23
  402. expect(result.value).toBe('1,23')
  403. })
  404. test('map', () => {
  405. const shallow = shallowReactive([1, 2, 3, 4])
  406. let result = computed(() => shallow.map(x => x ** 2))
  407. expect(result.value).toStrictEqual([1, 4, 9, 16])
  408. shallow[2] = 0
  409. expect(result.value).toStrictEqual([1, 4, 0, 16])
  410. const deep = reactive([{ val: 1 }, { val: 2 }])
  411. result = computed(() => deep.map(x => x.val ** 2))
  412. expect(result.value).toStrictEqual([1, 4])
  413. deep[1].val = 3
  414. expect(result.value).toStrictEqual([1, 9])
  415. })
  416. test('reduce left and right', () => {
  417. function toString(this: any) {
  418. return this.val + '-'
  419. }
  420. const shallow = shallowReactive([
  421. { val: 1, toString },
  422. { val: 2, toString },
  423. ] as any[])
  424. expect(shallow.reduce((acc, x) => acc + '' + x.val, undefined)).toBe(
  425. 'undefined12',
  426. )
  427. let left = computed(() => shallow.reduce((acc, x) => acc + '' + x.val))
  428. let right = computed(() =>
  429. shallow.reduceRight((acc, x) => acc + '' + x.val),
  430. )
  431. expect(left.value).toBe('1-2')
  432. expect(right.value).toBe('2-1')
  433. shallow[1].val = 23
  434. expect(left.value).toBe('1-2')
  435. expect(right.value).toBe('2-1')
  436. shallow.pop()
  437. expect(left.value).toBe(shallow[0])
  438. expect(right.value).toBe(shallow[0])
  439. const deep = reactive([{ val: 1 }, { val: 2 }])
  440. left = computed(() => deep.reduce((acc, x) => acc + x.val, '0'))
  441. right = computed(() => deep.reduceRight((acc, x) => acc + x.val, '3'))
  442. expect(left.value).toBe('012')
  443. expect(right.value).toBe('321')
  444. deep[1].val = 23
  445. expect(left.value).toBe('0123')
  446. expect(right.value).toBe('3231')
  447. })
  448. test('some', () => {
  449. const shallow = shallowReactive([1, 2, 5])
  450. let result = computed(() => shallow.some(x => x > 4))
  451. expect(result.value).toBe(true)
  452. shallow.pop()
  453. expect(result.value).toBe(false)
  454. const deep = reactive([{ val: 1 }, { val: 5 }])
  455. result = computed(() => deep.some(x => x.val > 4))
  456. expect(result.value).toBe(true)
  457. deep[1].val = 2
  458. expect(result.value).toBe(false)
  459. })
  460. // Node 20+
  461. // @ts-expect-error tests are not limited to es2016
  462. test.skipIf(!Array.prototype.toReversed)('toReversed', () => {
  463. const array = reactive([1, { val: 2 }])
  464. const result = computed(() => (array as any).toReversed())
  465. expect(result.value).toStrictEqual([{ val: 2 }, 1])
  466. expect(isReactive(result.value[0])).toBe(true)
  467. array.splice(1, 1, 2)
  468. expect(result.value).toStrictEqual([2, 1])
  469. })
  470. // Node 20+
  471. // @ts-expect-error tests are not limited to es2016
  472. test.skipIf(!Array.prototype.toSorted)('toSorted', () => {
  473. // No comparer
  474. // @ts-expect-error
  475. expect(shallowReactive([2, 1, 3]).toSorted()).toStrictEqual([1, 2, 3])
  476. const shallow = shallowReactive([{ val: 2 }, { val: 1 }, { val: 3 }])
  477. let result: ComputedRef<{ val: number }[]>
  478. // @ts-expect-error
  479. result = computed(() => shallow.toSorted((a, b) => a.val - b.val))
  480. expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3])
  481. expect(isReactive(result.value[0])).toBe(false)
  482. shallow[0].val = 4
  483. expect(result.value.map(x => x.val)).toStrictEqual([1, 4, 3])
  484. shallow.pop()
  485. expect(result.value.map(x => x.val)).toStrictEqual([1, 4])
  486. const deep = reactive([{ val: 2 }, { val: 1 }, { val: 3 }])
  487. // @ts-expect-error
  488. result = computed(() => deep.toSorted((a, b) => a.val - b.val))
  489. expect(result.value.map(x => x.val)).toStrictEqual([1, 2, 3])
  490. expect(isReactive(result.value[0])).toBe(true)
  491. deep[0].val = 4
  492. expect(result.value.map(x => x.val)).toStrictEqual([1, 3, 4])
  493. })
  494. // Node 20+
  495. // @ts-expect-error tests are not limited to es2016
  496. test.skipIf(!Array.prototype.toSpliced)('toSpliced', () => {
  497. const array = reactive([1, 2, 3])
  498. // @ts-expect-error
  499. const result = computed(() => array.toSpliced(1, 1, -2))
  500. expect(result.value).toStrictEqual([1, -2, 3])
  501. array[0] = 0
  502. expect(result.value).toStrictEqual([0, -2, 3])
  503. })
  504. test('values', () => {
  505. const shallow = shallowReactive([{ val: 1 }, { val: 2 }])
  506. const result = computed(() => Array.from(shallow.values()))
  507. expect(result.value).toStrictEqual([{ val: 1 }, { val: 2 }])
  508. expect(isReactive(result.value[0])).toBe(false)
  509. shallow.pop()
  510. expect(result.value).toStrictEqual([{ val: 1 }])
  511. const deep = reactive([{ val: 1 }, { val: 2 }])
  512. const firstItem = Array.from(deep.values())[0]
  513. expect(isReactive(firstItem)).toBe(true)
  514. })
  515. })
  516. })