@svgrid/grid 3.0.5 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +23 -3
  4. package/dist/SvGrid.controller.svelte.js +235 -54
  5. package/dist/SvGrid.css +143 -7
  6. package/dist/SvGrid.helpers.js +2 -4
  7. package/dist/SvGrid.svelte +223 -38
  8. package/dist/SvGrid.types.d.ts +77 -4
  9. package/dist/SvGridDropdown.svelte +20 -0
  10. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  11. package/dist/SvModal.svelte +22 -0
  12. package/dist/SvModal.svelte.d.ts +7 -0
  13. package/dist/a11y/dismissable.d.ts +10 -0
  14. package/dist/a11y/dismissable.js +24 -2
  15. package/dist/build-api.js +81 -31
  16. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
  17. package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
  18. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  19. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  21. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
  23. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
  25. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
  27. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
  28. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  29. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
  31. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
  32. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  33. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  34. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  35. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  36. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
  37. package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  39. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  40. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
  41. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
  42. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  43. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  44. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
  45. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  46. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  47. package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
  48. package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
  49. package/dist/cdn/svgrid.js +18 -18
  50. package/dist/cdn/svgrid.svelte-external.js +18 -18
  51. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  52. package/dist/cell-formatting.d.ts +9 -0
  53. package/dist/cell-formatting.js +52 -1
  54. package/dist/cell-render.js +25 -6
  55. package/dist/clipboard.d.ts +2 -2
  56. package/dist/clipboard.js +72 -23
  57. package/dist/columns.js +7 -2
  58. package/dist/conditional-formatting.d.ts +2 -0
  59. package/dist/conditional-formatting.js +2 -0
  60. package/dist/core.d.ts +22 -6
  61. package/dist/core.js +478 -40
  62. package/dist/editing.js +24 -0
  63. package/dist/export-format.d.ts +8 -0
  64. package/dist/export-format.js +23 -16
  65. package/dist/grid-messages.d.ts +4 -0
  66. package/dist/grid-messages.js +4 -0
  67. package/dist/headless.d.ts +5 -0
  68. package/dist/headless.js +12 -0
  69. package/dist/history.d.ts +21 -0
  70. package/dist/history.js +34 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +6 -0
  77. package/dist/row-resize.js +22 -1
  78. package/dist/selection.js +1 -1
  79. package/dist/server-block-cache.d.ts +5 -0
  80. package/dist/server-block-cache.js +8 -0
  81. package/dist/server-data-source.d.ts +35 -3
  82. package/dist/server-data-source.js +11 -2
  83. package/dist/server.d.ts +1 -1
  84. package/dist/sparkline.js +7 -5
  85. package/dist/svgrid-wrapper.types.d.ts +33 -0
  86. package/dist/validate.js +9 -1
  87. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  88. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  89. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  90. package/dist/virtualization/virtualizer.d.ts +3 -0
  91. package/dist/virtualization/virtualizer.js +14 -1
  92. package/package.json +6 -1
  93. package/src/GridMenus.svelte +34 -4
  94. package/src/SvGrid.controller.svelte.ts +236 -57
  95. package/src/SvGrid.css +143 -7
  96. package/src/SvGrid.helpers.ts +2 -4
  97. package/src/SvGrid.svelte +223 -38
  98. package/src/SvGrid.types.ts +77 -4
  99. package/src/SvGridDropdown.svelte +20 -0
  100. package/src/SvModal.svelte +22 -0
  101. package/src/SvModal.test.ts +30 -1
  102. package/src/a11y/dismissable.test.ts +10 -1
  103. package/src/a11y/dismissable.ts +22 -2
  104. package/src/build-api.ts +85 -27
  105. package/src/cell-formatting.test.ts +17 -0
  106. package/src/cell-formatting.ts +49 -1
  107. package/src/cell-render.test.ts +24 -2
  108. package/src/cell-render.ts +24 -8
  109. package/src/clipboard.test.ts +107 -0
  110. package/src/clipboard.ts +62 -23
  111. package/src/columns.test.ts +4 -4
  112. package/src/columns.ts +7 -2
  113. package/src/conditional-formatting.test.ts +8 -0
  114. package/src/conditional-formatting.ts +3 -0
  115. package/src/core.sort.test.ts +86 -0
  116. package/src/core.tick-repair.test.ts +287 -0
  117. package/src/core.ts +478 -40
  118. package/src/editing.test.ts +52 -0
  119. package/src/editing.ts +25 -0
  120. package/src/export-dates.test.ts +114 -0
  121. package/src/export-format.ts +26 -14
  122. package/src/grid-messages.test.ts +7 -0
  123. package/src/grid-messages.ts +10 -0
  124. package/src/headless.ts +24 -0
  125. package/src/history.test.ts +49 -1
  126. package/src/history.ts +48 -2
  127. package/src/index.ts +2 -0
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +6 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.ts +1 -1
  137. package/src/server-block-cache.test.ts +27 -0
  138. package/src/server-block-cache.ts +13 -0
  139. package/src/server-data-source.infinite.test.ts +62 -0
  140. package/src/server-data-source.test.ts +19 -0
  141. package/src/server-data-source.ts +48 -4
  142. package/src/server.ts +2 -0
  143. package/src/sparkline.test.ts +7 -0
  144. package/src/sparkline.ts +6 -4
  145. package/src/svgrid-wrapper.types.ts +32 -0
  146. package/src/svgrid.api.test.ts +21 -0
  147. package/src/svgrid.charting.test.ts +12 -4
  148. package/src/svgrid.context-menu.test.ts +12 -0
  149. package/src/svgrid.data-push.svelte.test.ts +94 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +1 -1
  153. package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  156. package/src/svgrid.row-pinning.test.ts +15 -1
  157. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  158. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  159. package/src/svgrid.upsell-license.test.ts +1 -1
  160. package/src/test-setup.ts +12 -0
  161. package/src/transaction.test.ts +58 -0
  162. package/src/validate.ts +7 -1
  163. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  164. package/src/virtualization/virtualizer.test.ts +44 -0
  165. package/src/virtualization/virtualizer.ts +13 -1
  166. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
@@ -0,0 +1,287 @@
1
+ /**
2
+ * A tick - a new data array where a few row objects were replaced - repairs
3
+ * the filtered and sorted stages' previous output instead of recomputing it,
4
+ * and the repaired output is exactly what the full pipeline would produce.
5
+ *
6
+ * The full pipeline is the oracle: for every case below a second grid is
7
+ * built fresh from the ticked data and its rows compared, object by object
8
+ * (same `original`, same order), against the ticked grid's rows. Sort keys
9
+ * of every kind the sorted stage specialises (number, date, ranked text,
10
+ * unranked text, a custom comparator), ties, descending, several clauses,
11
+ * an active filter, and the cases that must NOT repair: an add, a remove, a
12
+ * replacement whose filter membership flips, and a text value the ranking
13
+ * has not seen.
14
+ */
15
+ import { describe, expect, it } from 'vitest'
16
+ import {
17
+ createCoreRowModel,
18
+ createFilteredRowModel,
19
+ createSortedRowModel,
20
+ createSvGridCore,
21
+ rowSortingFeature,
22
+ columnFilteringFeature,
23
+ tableFeatures,
24
+ sortFns,
25
+ type ColumnDef,
26
+ type SortingState,
27
+ type ColumnFiltersState,
28
+ } from './core'
29
+
30
+ type Quote = { id: number; symbol: string; sector: string; price: number; asOf: string; note: string }
31
+
32
+ const SECTORS = ['Tech', 'Energy', 'Health', 'Finance']
33
+ function makeRows(n: number, seed = 1): Quote[] {
34
+ let s = seed
35
+ const rand = () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff
36
+ return Array.from({ length: n }, (_, i) => ({
37
+ id: i,
38
+ symbol: `S${String(i).padStart(4, '0')}`,
39
+ sector: SECTORS[Math.floor(rand() * SECTORS.length)]!,
40
+ // Many ties on purpose: the stable tie-break is the thing to get right.
41
+ price: Math.round(rand() * 50),
42
+ asOf: new Date(Date.UTC(2026, 0, 1 + Math.floor(rand() * 200))).toISOString(),
43
+ note: `n${Math.floor(rand() * 1e6)}`,
44
+ }))
45
+ }
46
+
47
+ const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
48
+ const COLUMNS = [
49
+ { field: 'id', editorType: 'number' },
50
+ { field: 'symbol' },
51
+ { field: 'sector' },
52
+ { field: 'price', editorType: 'number' },
53
+ { field: 'asOf', editorType: 'date' },
54
+ { field: 'note' },
55
+ ] as unknown as Array<ColumnDef<typeof features, Quote>>
56
+
57
+ function makeGrid(
58
+ data: Quote[],
59
+ state: { sorting?: SortingState; columnFilters?: ColumnFiltersState },
60
+ fns: typeof sortFns = sortFns,
61
+ ) {
62
+ let sortRuns = 0
63
+ const sorted = createSortedRowModel<Quote>(fns)
64
+ // A live getter, as <SvGrid> passes it: getRowModel reads options.data on
65
+ // every call, so replacing the array here is what a tick looks like.
66
+ const ref = { data }
67
+ const grid = createSvGridCore<typeof features, Quote>({
68
+ _features: features,
69
+ _rowModels: {
70
+ coreRowModel: createCoreRowModel(),
71
+ filteredRowModel: createFilteredRowModel(),
72
+ sortedRowModel: (args) => {
73
+ sortRuns++
74
+ return sorted(args)
75
+ },
76
+ },
77
+ columns: COLUMNS,
78
+ get data() {
79
+ return ref.data
80
+ },
81
+ state: { sorting: [], columnFilters: [], ...state },
82
+ })
83
+ return { grid, sortRuns: () => sortRuns, set: (next: Quote[]) => (ref.data = next) }
84
+ }
85
+
86
+ /** The ticked data: the same array with `count` rows replaced by new objects. */
87
+ function tick(data: Quote[], count: number, mutate: (q: Quote, i: number) => Partial<Quote>, seed = 7): Quote[] {
88
+ let s = seed
89
+ const rand = () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff
90
+ const next = data.slice()
91
+ const picked = new Set<number>()
92
+ while (picked.size < count) picked.add(Math.floor(rand() * data.length))
93
+ for (const i of picked) next[i] = { ...data[i]!, ...mutate(data[i]!, i) }
94
+ return next
95
+ }
96
+
97
+ function originals(grid: ReturnType<typeof makeGrid>['grid']) {
98
+ return grid.getRowModel().rows.map((r) => r.original)
99
+ }
100
+
101
+ function expectRepairedEqualsFull(
102
+ before: Quote[],
103
+ after: Quote[],
104
+ state: { sorting?: SortingState; columnFilters?: ColumnFiltersState },
105
+ fns?: typeof sortFns,
106
+ ) {
107
+ const ticked = makeGrid(before, state, fns)
108
+ ticked.grid.getRowModel()
109
+ ticked.set(after)
110
+ const repaired = originals(ticked.grid)
111
+
112
+ const oracle = makeGrid(after, state, fns)
113
+ const full = originals(oracle.grid)
114
+
115
+ expect(repaired.length).toBe(full.length)
116
+ for (let i = 0; i < full.length; i++) {
117
+ if (repaired[i] !== full[i]) {
118
+ throw new Error(`row ${i} differs: repaired id=${repaired[i]?.id} price=${repaired[i]?.price}, full id=${full[i]?.id} price=${full[i]?.price}`)
119
+ }
120
+ }
121
+ return ticked
122
+ }
123
+
124
+ describe('tick repair: sorted stage equals a full sort', () => {
125
+ const price = (q: Quote) => ({ price: (q.price * 7 + 13) % 50 })
126
+
127
+ it('numeric ascending, with ties', () => {
128
+ const before = makeRows(2000)
129
+ expectRepairedEqualsFull(before, tick(before, 150, price), { sorting: [{ id: 'price', desc: false }] })
130
+ })
131
+
132
+ it('numeric descending', () => {
133
+ const before = makeRows(2000)
134
+ expectRepairedEqualsFull(before, tick(before, 150, price), { sorting: [{ id: 'price', desc: true }] })
135
+ })
136
+
137
+ it('date column', () => {
138
+ const before = makeRows(1500)
139
+ expectRepairedEqualsFull(
140
+ before,
141
+ tick(before, 90, (q) => ({ asOf: new Date(Date.parse(q.asOf) + 86_400_000 * 37).toISOString() })),
142
+ { sorting: [{ id: 'asOf', desc: false }] },
143
+ )
144
+ })
145
+
146
+ it('ranked text (few distinct values) when the new value is a known one', () => {
147
+ const before = makeRows(2000)
148
+ expectRepairedEqualsFull(
149
+ before,
150
+ tick(before, 120, (q) => ({ sector: SECTORS[(SECTORS.indexOf(q.sector) + 1) % SECTORS.length]! })),
151
+ { sorting: [{ id: 'sector', desc: false }, { id: 'price', desc: true }] },
152
+ )
153
+ })
154
+
155
+ it('unranked text (mostly distinct values)', () => {
156
+ const before = makeRows(1200)
157
+ expectRepairedEqualsFull(before, tick(before, 60, (q, i) => ({ note: `z${i}-${q.note}` })), {
158
+ sorting: [{ id: 'note', desc: false }],
159
+ })
160
+ })
161
+
162
+ it('a custom comparator', () => {
163
+ const before = makeRows(800)
164
+ const custom = { ...sortFns, number: (a: unknown, b: unknown) => (Number(b) % 10) - (Number(a) % 10) }
165
+ expectRepairedEqualsFull(before, tick(before, 40, price), { sorting: [{ id: 'price', desc: false }] }, custom)
166
+ })
167
+
168
+ it('three clauses', () => {
169
+ const before = makeRows(3000)
170
+ expectRepairedEqualsFull(before, tick(before, 200, price), {
171
+ sorting: [{ id: 'sector', desc: true }, { id: 'price', desc: false }, { id: 'id', desc: true }],
172
+ })
173
+ })
174
+
175
+ it('a replacement that leaves the key unchanged keeps its slot', () => {
176
+ const before = makeRows(500)
177
+ expectRepairedEqualsFull(before, tick(before, 30, (q) => ({ note: q.note + '!' })), {
178
+ sorting: [{ id: 'price', desc: false }],
179
+ })
180
+ })
181
+
182
+ it('back-to-back ticks repair on top of a repair', () => {
183
+ const before = makeRows(2000)
184
+ const t1 = tick(before, 100, price, 3)
185
+ const t2 = tick(t1, 100, price, 5)
186
+ const t3 = tick(t2, 100, price, 9)
187
+ const g = makeGrid(before, { sorting: [{ id: 'price', desc: true }] })
188
+ g.grid.getRowModel()
189
+ for (const d of [t1, t2, t3]) g.set(d)
190
+ const full = originals(makeGrid(t3, { sorting: [{ id: 'price', desc: true }] }).grid)
191
+ expect(originals(g.grid)).toEqual(full)
192
+ })
193
+ })
194
+
195
+ describe('tick repair: filtered stage', () => {
196
+ const filters: ColumnFiltersState = [{ id: 'sector', value: 'Tech' }]
197
+
198
+ it('keeps a replaced row in its slot when it still passes', () => {
199
+ const before = makeRows(2000)
200
+ const g = expectRepairedEqualsFull(
201
+ before,
202
+ tick(before, 100, (q) => ({ price: q.price + 1 })),
203
+ { columnFilters: filters, sorting: [{ id: 'price', desc: false }] },
204
+ )
205
+ expect(g.sortRuns()).toBe(2)
206
+ })
207
+
208
+ it('falls back to the full pass when a replacement changes membership', () => {
209
+ const before = makeRows(2000)
210
+ expectRepairedEqualsFull(
211
+ before,
212
+ tick(before, 50, (q) => ({ sector: q.sector === 'Tech' ? 'Energy' : 'Tech' })),
213
+ { columnFilters: filters, sorting: [{ id: 'price', desc: false }] },
214
+ )
215
+ })
216
+ })
217
+
218
+ describe('tick repair: when it must not happen', () => {
219
+ it('an add goes through the full pipeline', () => {
220
+ const before = makeRows(1000)
221
+ const after = [...before, { ...before[0]!, id: 1000, symbol: 'S1000', price: 25 }]
222
+ expectRepairedEqualsFull(before, after, { sorting: [{ id: 'price', desc: false }] })
223
+ })
224
+
225
+ it('a remove goes through the full pipeline', () => {
226
+ const before = makeRows(1000)
227
+ const after = before.filter((_, i) => i !== 17)
228
+ expectRepairedEqualsFull(before, after, { sorting: [{ id: 'price', desc: false }] })
229
+ })
230
+
231
+ it('a ranked text column with a value the ranking has not seen', () => {
232
+ const before = makeRows(2000)
233
+ expectRepairedEqualsFull(before, tick(before, 10, () => ({ sector: 'Utilities' })), {
234
+ sorting: [{ id: 'sector', desc: false }],
235
+ })
236
+ })
237
+
238
+ it('more than a quarter of the rows replaced', () => {
239
+ const before = makeRows(1000)
240
+ expectRepairedEqualsFull(before, tick(before, 400, (q) => ({ price: q.price + 3 })), {
241
+ sorting: [{ id: 'price', desc: false }],
242
+ })
243
+ })
244
+
245
+ it('a sorting change between ticks', () => {
246
+ const before = makeRows(1000)
247
+ const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
248
+ g.grid.getRowModel()
249
+ const after = tick(before, 50, (q) => ({ price: q.price + 5 }))
250
+ g.grid.store.setState((prev) => ({ ...prev, sorting: [{ id: 'price', desc: true }] }))
251
+ g.set(after)
252
+ expect(originals(g.grid)).toEqual(originals(makeGrid(after, { sorting: [{ id: 'price', desc: true }] }).grid))
253
+ })
254
+
255
+ it('a new array with every object the same re-reads the rows (the in-place refresh idiom)', () => {
256
+ // `rows = [...rows]` after mutating objects in place is the documented
257
+ // refresh (docs/help/cells/view-refresh.md). Nothing was replaced, so
258
+ // there is no repair; the stages recompute from the current values and
259
+ // the mutation is picked up.
260
+ const before = makeRows(200)
261
+ const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
262
+ g.grid.getRowModel()
263
+ before[0]!.price = 999
264
+ g.set(before.slice())
265
+ const rows = originals(g.grid)
266
+ expect(rows[rows.length - 1]).toBe(before[0])
267
+ expect(g.sortRuns()).toBe(2)
268
+ })
269
+
270
+ it('a kept row is assumed unchanged in value: the immutable-update contract', () => {
271
+ // Replace one object and mutate another in place in the same tick. The
272
+ // replaced one lands in sorted position; the mutated one keeps the slot
273
+ // its old key earned, because the repair only reads the rows it was told
274
+ // changed. A feed that mutates in place must not mix in replacements, or
275
+ // must pass a new array with no replacements to refresh.
276
+ const before = makeRows(200)
277
+ const g = makeGrid(before, { sorting: [{ id: 'price', desc: false }] })
278
+ g.grid.getRowModel()
279
+ const next = before.slice()
280
+ next[5] = { ...before[5]!, price: 999 }
281
+ before[7]!.price = -1
282
+ g.set(next)
283
+ const rows = originals(g.grid)
284
+ expect(rows[rows.length - 1]).toBe(next[5])
285
+ expect(rows[0]).not.toBe(before[7])
286
+ })
287
+ })