reactiveArray.spec.ts 18 KB

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