@svgrid/grid 2.2.27 → 2.2.28
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.
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +734 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2723 -2723
- package/dist/SvGrid.svelte +3333 -3333
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +287 -287
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/{GridMenus-BzxEYNtW.js → GridMenus-BbzEvTYO.js} +2 -2
- package/dist/cdn/{GridMenus-Dr-nGEXn.js → GridMenus-E220X2kM.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BxNEslEo.js} +25 -25
- package/dist/cdn/{src-Cw8d9U8j.js → src-C4yKQZ5t.js} +26 -26
- package/dist/cdn/svgrid.js +5 -5
- package/dist/cdn/svgrid.svelte-external.js +5 -5
- package/dist/chart-export.js +8 -8
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +734 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3239 -3239
- package/src/SvGrid.css +2723 -2723
- package/src/SvGrid.svelte +3333 -3333
- package/src/SvGrid.types.ts +1613 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +287 -287
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +713 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +700 -700
- package/src/menus.ts +652 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/themes/ember.css +71 -71
|
@@ -1,289 +1,289 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { createServerDataSource, type ServerDataSource } from './server-data-source'
|
|
3
|
-
|
|
4
|
-
type Row = { id: number }
|
|
5
|
-
|
|
6
|
-
function makeSource(total = 250, delay = 0): ServerDataSource<Row> & { calls: number } {
|
|
7
|
-
const all: Row[] = Array.from({ length: total }, (_, i) => ({ id: i }))
|
|
8
|
-
return {
|
|
9
|
-
calls: 0,
|
|
10
|
-
async getRows(req) {
|
|
11
|
-
this.calls += 1
|
|
12
|
-
if (delay) await new Promise((r) => setTimeout(r, delay))
|
|
13
|
-
// server applies sort: reverse when desc on 'id'
|
|
14
|
-
let rows = all
|
|
15
|
-
if (req.sortModel[0]?.id === 'id' && req.sortModel[0]?.desc) rows = [...all].reverse()
|
|
16
|
-
return { rows: rows.slice(req.startRow, req.endRow), rowCount: total }
|
|
17
|
-
},
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const flush = () => new Promise((r) => setTimeout(r, 0))
|
|
22
|
-
|
|
23
|
-
describe('createServerDataSource', () => {
|
|
24
|
-
it('fetches a page and reports total + pageCount', async () => {
|
|
25
|
-
const src = makeSource(250)
|
|
26
|
-
const states: any[] = []
|
|
27
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) })
|
|
28
|
-
ctl.refresh()
|
|
29
|
-
await flush()
|
|
30
|
-
const last = ctl.getState()
|
|
31
|
-
expect(last.rows).toHaveLength(50)
|
|
32
|
-
expect(last.total).toBe(250)
|
|
33
|
-
expect(last.pageCount).toBe(5)
|
|
34
|
-
expect(last.loading).toBe(false)
|
|
35
|
-
// emitted a loading=true then loading=false
|
|
36
|
-
expect(states.some((s) => s.loading)).toBe(true)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('dispose() during an in-flight fetch clears the loading state (#83)', async () => {
|
|
40
|
-
const src = makeSource(250, 20) // 20ms delay keeps the fetch in-flight
|
|
41
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
42
|
-
ctl.refresh()
|
|
43
|
-
expect(ctl.getState().loading).toBe(true)
|
|
44
|
-
ctl.dispose()
|
|
45
|
-
expect(ctl.getState().loading).toBe(false)
|
|
46
|
-
await new Promise((r) => setTimeout(r, 30)) // let the fetch settle
|
|
47
|
-
expect(ctl.getState().loading).toBe(false) // stays cleared, not stuck
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('setPage fetches the right slice', async () => {
|
|
51
|
-
const src = makeSource(250)
|
|
52
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
53
|
-
ctl.setPage(2)
|
|
54
|
-
await flush()
|
|
55
|
-
expect(ctl.getState().rows[0]).toEqual({ id: 100 })
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('setSort resets to page 0 and re-fetches', async () => {
|
|
59
|
-
const src = makeSource(250)
|
|
60
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
61
|
-
ctl.setPage(2)
|
|
62
|
-
await flush()
|
|
63
|
-
ctl.setSort([{ id: 'id', desc: true }])
|
|
64
|
-
await flush()
|
|
65
|
-
const s = ctl.getState()
|
|
66
|
-
expect(s.pageIndex).toBe(0)
|
|
67
|
-
expect(s.rows[0]).toEqual({ id: 249 }) // reversed
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('ignores a stale response when a newer request supersedes it', async () => {
|
|
71
|
-
// slow source: first call (page 0) resolves AFTER the second (page 1)
|
|
72
|
-
const all = Array.from({ length: 100 }, (_, i) => ({ id: i }))
|
|
73
|
-
let call = 0
|
|
74
|
-
const src: ServerDataSource<Row> = {
|
|
75
|
-
getRows: vi.fn(async (req) => {
|
|
76
|
-
call += 1
|
|
77
|
-
const delay = call === 1 ? 40 : 5 // first call is slow
|
|
78
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
79
|
-
return { rows: all.slice(req.startRow, req.endRow), rowCount: 100 }
|
|
80
|
-
}),
|
|
81
|
-
}
|
|
82
|
-
const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => {} })
|
|
83
|
-
ctl.refresh() // page 0 (slow)
|
|
84
|
-
ctl.setPage(3) // page 30 (fast) - should win
|
|
85
|
-
await new Promise((r) => setTimeout(r, 80))
|
|
86
|
-
expect(ctl.getState().rows[0]).toEqual({ id: 30 })
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('dispose stops further updates', async () => {
|
|
90
|
-
const src = makeSource(50, 20)
|
|
91
|
-
const onChange = vi.fn()
|
|
92
|
-
const ctl = createServerDataSource(src, { pageSize: 10, onChange })
|
|
93
|
-
ctl.refresh()
|
|
94
|
-
ctl.dispose()
|
|
95
|
-
await new Promise((r) => setTimeout(r, 40))
|
|
96
|
-
// only the synchronous loading=true emit happened before dispose
|
|
97
|
-
expect(ctl.getState().rows).toHaveLength(0)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('createServerDataSource mutations', () => {
|
|
102
|
-
type URow = { id: number; name: string }
|
|
103
|
-
|
|
104
|
-
function writable() {
|
|
105
|
-
let store: URow[] = [
|
|
106
|
-
{ id: 0, name: 'a' },
|
|
107
|
-
{ id: 1, name: 'b' },
|
|
108
|
-
{ id: 2, name: 'c' },
|
|
109
|
-
]
|
|
110
|
-
return {
|
|
111
|
-
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
112
|
-
rows: store.slice(req.startRow, req.endRow),
|
|
113
|
-
rowCount: store.length,
|
|
114
|
-
})),
|
|
115
|
-
createRow: vi.fn(async (input: Partial<URow>) => {
|
|
116
|
-
const row: URow = { id: store.length, name: input.name ?? '' }
|
|
117
|
-
store = [...store, row]
|
|
118
|
-
return row
|
|
119
|
-
}),
|
|
120
|
-
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
121
|
-
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
122
|
-
return store.find((r) => String(r.id) === id)!
|
|
123
|
-
}),
|
|
124
|
-
deleteRow: vi.fn(async (id: string) => {
|
|
125
|
-
store = store.filter((r) => String(r.id) !== id)
|
|
126
|
-
}),
|
|
127
|
-
} as unknown as ServerDataSource<URow> & {
|
|
128
|
-
getRows: ReturnType<typeof vi.fn>
|
|
129
|
-
createRow: ReturnType<typeof vi.fn>
|
|
130
|
-
updateRow: ReturnType<typeof vi.fn>
|
|
131
|
-
deleteRow: ReturnType<typeof vi.fn>
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
it('createRow writes through and refreshes the current page', async () => {
|
|
136
|
-
const src = writable()
|
|
137
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
138
|
-
ctl.refresh()
|
|
139
|
-
await flush()
|
|
140
|
-
const created = await ctl.createRow({ name: 'z' })
|
|
141
|
-
expect(created).toEqual({ id: 3, name: 'z' })
|
|
142
|
-
expect(src.createRow).toHaveBeenCalledWith({ name: 'z' })
|
|
143
|
-
// the follow-up refresh re-fetched, so total + rows reflect the write
|
|
144
|
-
expect(ctl.getState().total).toBe(4)
|
|
145
|
-
expect(ctl.getState().rows.some((r) => r.name === 'z')).toBe(true)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('toggles `saving` around a mutation', async () => {
|
|
149
|
-
const src = writable()
|
|
150
|
-
const states: Array<{ saving: boolean }> = []
|
|
151
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) })
|
|
152
|
-
const p = ctl.createRow({ name: 'x' })
|
|
153
|
-
expect(ctl.getState().saving).toBe(true) // synchronous emit before the await
|
|
154
|
-
await p
|
|
155
|
-
expect(ctl.getState().saving).toBe(false)
|
|
156
|
-
expect(states.some((s) => s.saving)).toBe(true)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('updateRow and deleteRow call through and refresh', async () => {
|
|
160
|
-
const src = writable()
|
|
161
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
162
|
-
ctl.refresh()
|
|
163
|
-
await flush()
|
|
164
|
-
const updated = await ctl.updateRow('1', { name: 'B!' })
|
|
165
|
-
expect(updated).toEqual({ id: 1, name: 'B!' })
|
|
166
|
-
expect(src.updateRow).toHaveBeenCalledWith('1', { name: 'B!' })
|
|
167
|
-
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
168
|
-
|
|
169
|
-
await ctl.deleteRow('0')
|
|
170
|
-
expect(src.deleteRow).toHaveBeenCalledWith('0')
|
|
171
|
-
expect(ctl.getState().total).toBe(2)
|
|
172
|
-
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('rejects when the source has no matching writer', async () => {
|
|
176
|
-
const readonly: ServerDataSource<URow> = {
|
|
177
|
-
getRows: async () => ({ rows: [], rowCount: 0 }),
|
|
178
|
-
}
|
|
179
|
-
const ctl = createServerDataSource(readonly, { onChange: () => {} })
|
|
180
|
-
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow(/does not implement createRow/)
|
|
181
|
-
await expect(ctl.updateRow('1', {})).rejects.toThrow(/does not implement updateRow/)
|
|
182
|
-
await expect(ctl.deleteRow('1')).rejects.toThrow(/does not implement deleteRow/)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('rejects mutations after dispose, without calling the source', async () => {
|
|
186
|
-
const src = writable()
|
|
187
|
-
const ctl = createServerDataSource(src, { onChange: () => {} })
|
|
188
|
-
ctl.dispose()
|
|
189
|
-
await expect(ctl.deleteRow('0')).rejects.toThrow(/disposed/)
|
|
190
|
-
expect(src.deleteRow).not.toHaveBeenCalled()
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('leaves read state untouched when a mutation fails', async () => {
|
|
194
|
-
const src = writable()
|
|
195
|
-
src.createRow.mockRejectedValueOnce(new Error('boom'))
|
|
196
|
-
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
197
|
-
ctl.refresh()
|
|
198
|
-
await flush()
|
|
199
|
-
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow('boom')
|
|
200
|
-
expect(ctl.getState().saving).toBe(false)
|
|
201
|
-
expect(ctl.getState().total).toBe(3) // unchanged; no refresh on failure
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
describe('createServerDataSource optimistic mutations', () => {
|
|
206
|
-
type URow = { id: number; name: string }
|
|
207
|
-
const opts = (onChange: (s: any) => void = () => {}) => ({
|
|
208
|
-
pageSize: 50,
|
|
209
|
-
optimistic: true,
|
|
210
|
-
getRowId: (r: URow) => String(r.id),
|
|
211
|
-
onChange,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
function writable() {
|
|
215
|
-
let store: URow[] = [
|
|
216
|
-
{ id: 0, name: 'a' },
|
|
217
|
-
{ id: 1, name: 'b' },
|
|
218
|
-
{ id: 2, name: 'c' },
|
|
219
|
-
]
|
|
220
|
-
return {
|
|
221
|
-
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
222
|
-
rows: store.slice(req.startRow, req.endRow),
|
|
223
|
-
rowCount: store.length,
|
|
224
|
-
})),
|
|
225
|
-
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
226
|
-
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
227
|
-
return store.find((r) => String(r.id) === id)!
|
|
228
|
-
}),
|
|
229
|
-
deleteRow: vi.fn(async (id: string) => {
|
|
230
|
-
store = store.filter((r) => String(r.id) !== id)
|
|
231
|
-
}),
|
|
232
|
-
} as any
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
it('applies an update to local rows immediately, before the server resolves', async () => {
|
|
236
|
-
const src = writable()
|
|
237
|
-
src.updateRow.mockImplementationOnce(
|
|
238
|
-
(id: string, patch: any) => new Promise((r) => setTimeout(() => r({ id: Number(id), ...patch }), 30)),
|
|
239
|
-
)
|
|
240
|
-
const ctl = createServerDataSource(src, opts())
|
|
241
|
-
ctl.refresh()
|
|
242
|
-
await flush()
|
|
243
|
-
const p = ctl.updateRow('1', { name: 'B!' })
|
|
244
|
-
// synchronously reflected, no refetch
|
|
245
|
-
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
246
|
-
expect(ctl.getState().saving).toBe(true)
|
|
247
|
-
await p
|
|
248
|
-
expect(ctl.getState().saving).toBe(false)
|
|
249
|
-
// getRows only called once (the initial refresh) - no refetch for the update
|
|
250
|
-
expect(src.getRows).toHaveBeenCalledTimes(1)
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('rolls an update back on error', async () => {
|
|
254
|
-
const src = writable()
|
|
255
|
-
src.updateRow.mockRejectedValueOnce(new Error('nope'))
|
|
256
|
-
const ctl = createServerDataSource(src, opts())
|
|
257
|
-
ctl.refresh()
|
|
258
|
-
await flush()
|
|
259
|
-
await expect(ctl.updateRow('1', { name: 'B!' })).rejects.toThrow('nope')
|
|
260
|
-
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('b') // restored
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('removes a row immediately on delete and decrements total, restoring on error', async () => {
|
|
264
|
-
const src = writable()
|
|
265
|
-
const ctl = createServerDataSource(src, opts())
|
|
266
|
-
ctl.refresh()
|
|
267
|
-
await flush()
|
|
268
|
-
|
|
269
|
-
const p = ctl.deleteRow('0')
|
|
270
|
-
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
271
|
-
expect(ctl.getState().total).toBe(2)
|
|
272
|
-
await p
|
|
273
|
-
|
|
274
|
-
src.deleteRow.mockRejectedValueOnce(new Error('locked'))
|
|
275
|
-
await expect(ctl.deleteRow('1')).rejects.toThrow('locked')
|
|
276
|
-
expect(ctl.getState().rows.some((r) => r.id === 1)).toBe(true) // restored
|
|
277
|
-
expect(ctl.getState().total).toBe(2)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('falls back to a refetch when the row is not on the current page', async () => {
|
|
281
|
-
const src = writable()
|
|
282
|
-
const ctl = createServerDataSource(src, opts())
|
|
283
|
-
ctl.refresh()
|
|
284
|
-
await flush()
|
|
285
|
-
await ctl.updateRow('999', { name: 'ghost' }) // not present locally
|
|
286
|
-
// fell back to mutate() which refreshes: getRows called again
|
|
287
|
-
expect(src.getRows).toHaveBeenCalledTimes(2)
|
|
288
|
-
})
|
|
289
|
-
})
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createServerDataSource, type ServerDataSource } from './server-data-source'
|
|
3
|
+
|
|
4
|
+
type Row = { id: number }
|
|
5
|
+
|
|
6
|
+
function makeSource(total = 250, delay = 0): ServerDataSource<Row> & { calls: number } {
|
|
7
|
+
const all: Row[] = Array.from({ length: total }, (_, i) => ({ id: i }))
|
|
8
|
+
return {
|
|
9
|
+
calls: 0,
|
|
10
|
+
async getRows(req) {
|
|
11
|
+
this.calls += 1
|
|
12
|
+
if (delay) await new Promise((r) => setTimeout(r, delay))
|
|
13
|
+
// server applies sort: reverse when desc on 'id'
|
|
14
|
+
let rows = all
|
|
15
|
+
if (req.sortModel[0]?.id === 'id' && req.sortModel[0]?.desc) rows = [...all].reverse()
|
|
16
|
+
return { rows: rows.slice(req.startRow, req.endRow), rowCount: total }
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const flush = () => new Promise((r) => setTimeout(r, 0))
|
|
22
|
+
|
|
23
|
+
describe('createServerDataSource', () => {
|
|
24
|
+
it('fetches a page and reports total + pageCount', async () => {
|
|
25
|
+
const src = makeSource(250)
|
|
26
|
+
const states: any[] = []
|
|
27
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) })
|
|
28
|
+
ctl.refresh()
|
|
29
|
+
await flush()
|
|
30
|
+
const last = ctl.getState()
|
|
31
|
+
expect(last.rows).toHaveLength(50)
|
|
32
|
+
expect(last.total).toBe(250)
|
|
33
|
+
expect(last.pageCount).toBe(5)
|
|
34
|
+
expect(last.loading).toBe(false)
|
|
35
|
+
// emitted a loading=true then loading=false
|
|
36
|
+
expect(states.some((s) => s.loading)).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('dispose() during an in-flight fetch clears the loading state (#83)', async () => {
|
|
40
|
+
const src = makeSource(250, 20) // 20ms delay keeps the fetch in-flight
|
|
41
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
42
|
+
ctl.refresh()
|
|
43
|
+
expect(ctl.getState().loading).toBe(true)
|
|
44
|
+
ctl.dispose()
|
|
45
|
+
expect(ctl.getState().loading).toBe(false)
|
|
46
|
+
await new Promise((r) => setTimeout(r, 30)) // let the fetch settle
|
|
47
|
+
expect(ctl.getState().loading).toBe(false) // stays cleared, not stuck
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('setPage fetches the right slice', async () => {
|
|
51
|
+
const src = makeSource(250)
|
|
52
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
53
|
+
ctl.setPage(2)
|
|
54
|
+
await flush()
|
|
55
|
+
expect(ctl.getState().rows[0]).toEqual({ id: 100 })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('setSort resets to page 0 and re-fetches', async () => {
|
|
59
|
+
const src = makeSource(250)
|
|
60
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
61
|
+
ctl.setPage(2)
|
|
62
|
+
await flush()
|
|
63
|
+
ctl.setSort([{ id: 'id', desc: true }])
|
|
64
|
+
await flush()
|
|
65
|
+
const s = ctl.getState()
|
|
66
|
+
expect(s.pageIndex).toBe(0)
|
|
67
|
+
expect(s.rows[0]).toEqual({ id: 249 }) // reversed
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('ignores a stale response when a newer request supersedes it', async () => {
|
|
71
|
+
// slow source: first call (page 0) resolves AFTER the second (page 1)
|
|
72
|
+
const all = Array.from({ length: 100 }, (_, i) => ({ id: i }))
|
|
73
|
+
let call = 0
|
|
74
|
+
const src: ServerDataSource<Row> = {
|
|
75
|
+
getRows: vi.fn(async (req) => {
|
|
76
|
+
call += 1
|
|
77
|
+
const delay = call === 1 ? 40 : 5 // first call is slow
|
|
78
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
79
|
+
return { rows: all.slice(req.startRow, req.endRow), rowCount: 100 }
|
|
80
|
+
}),
|
|
81
|
+
}
|
|
82
|
+
const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => {} })
|
|
83
|
+
ctl.refresh() // page 0 (slow)
|
|
84
|
+
ctl.setPage(3) // page 30 (fast) - should win
|
|
85
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
86
|
+
expect(ctl.getState().rows[0]).toEqual({ id: 30 })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('dispose stops further updates', async () => {
|
|
90
|
+
const src = makeSource(50, 20)
|
|
91
|
+
const onChange = vi.fn()
|
|
92
|
+
const ctl = createServerDataSource(src, { pageSize: 10, onChange })
|
|
93
|
+
ctl.refresh()
|
|
94
|
+
ctl.dispose()
|
|
95
|
+
await new Promise((r) => setTimeout(r, 40))
|
|
96
|
+
// only the synchronous loading=true emit happened before dispose
|
|
97
|
+
expect(ctl.getState().rows).toHaveLength(0)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('createServerDataSource mutations', () => {
|
|
102
|
+
type URow = { id: number; name: string }
|
|
103
|
+
|
|
104
|
+
function writable() {
|
|
105
|
+
let store: URow[] = [
|
|
106
|
+
{ id: 0, name: 'a' },
|
|
107
|
+
{ id: 1, name: 'b' },
|
|
108
|
+
{ id: 2, name: 'c' },
|
|
109
|
+
]
|
|
110
|
+
return {
|
|
111
|
+
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
112
|
+
rows: store.slice(req.startRow, req.endRow),
|
|
113
|
+
rowCount: store.length,
|
|
114
|
+
})),
|
|
115
|
+
createRow: vi.fn(async (input: Partial<URow>) => {
|
|
116
|
+
const row: URow = { id: store.length, name: input.name ?? '' }
|
|
117
|
+
store = [...store, row]
|
|
118
|
+
return row
|
|
119
|
+
}),
|
|
120
|
+
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
121
|
+
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
122
|
+
return store.find((r) => String(r.id) === id)!
|
|
123
|
+
}),
|
|
124
|
+
deleteRow: vi.fn(async (id: string) => {
|
|
125
|
+
store = store.filter((r) => String(r.id) !== id)
|
|
126
|
+
}),
|
|
127
|
+
} as unknown as ServerDataSource<URow> & {
|
|
128
|
+
getRows: ReturnType<typeof vi.fn>
|
|
129
|
+
createRow: ReturnType<typeof vi.fn>
|
|
130
|
+
updateRow: ReturnType<typeof vi.fn>
|
|
131
|
+
deleteRow: ReturnType<typeof vi.fn>
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
it('createRow writes through and refreshes the current page', async () => {
|
|
136
|
+
const src = writable()
|
|
137
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
138
|
+
ctl.refresh()
|
|
139
|
+
await flush()
|
|
140
|
+
const created = await ctl.createRow({ name: 'z' })
|
|
141
|
+
expect(created).toEqual({ id: 3, name: 'z' })
|
|
142
|
+
expect(src.createRow).toHaveBeenCalledWith({ name: 'z' })
|
|
143
|
+
// the follow-up refresh re-fetched, so total + rows reflect the write
|
|
144
|
+
expect(ctl.getState().total).toBe(4)
|
|
145
|
+
expect(ctl.getState().rows.some((r) => r.name === 'z')).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('toggles `saving` around a mutation', async () => {
|
|
149
|
+
const src = writable()
|
|
150
|
+
const states: Array<{ saving: boolean }> = []
|
|
151
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) })
|
|
152
|
+
const p = ctl.createRow({ name: 'x' })
|
|
153
|
+
expect(ctl.getState().saving).toBe(true) // synchronous emit before the await
|
|
154
|
+
await p
|
|
155
|
+
expect(ctl.getState().saving).toBe(false)
|
|
156
|
+
expect(states.some((s) => s.saving)).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('updateRow and deleteRow call through and refresh', async () => {
|
|
160
|
+
const src = writable()
|
|
161
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
162
|
+
ctl.refresh()
|
|
163
|
+
await flush()
|
|
164
|
+
const updated = await ctl.updateRow('1', { name: 'B!' })
|
|
165
|
+
expect(updated).toEqual({ id: 1, name: 'B!' })
|
|
166
|
+
expect(src.updateRow).toHaveBeenCalledWith('1', { name: 'B!' })
|
|
167
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
168
|
+
|
|
169
|
+
await ctl.deleteRow('0')
|
|
170
|
+
expect(src.deleteRow).toHaveBeenCalledWith('0')
|
|
171
|
+
expect(ctl.getState().total).toBe(2)
|
|
172
|
+
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('rejects when the source has no matching writer', async () => {
|
|
176
|
+
const readonly: ServerDataSource<URow> = {
|
|
177
|
+
getRows: async () => ({ rows: [], rowCount: 0 }),
|
|
178
|
+
}
|
|
179
|
+
const ctl = createServerDataSource(readonly, { onChange: () => {} })
|
|
180
|
+
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow(/does not implement createRow/)
|
|
181
|
+
await expect(ctl.updateRow('1', {})).rejects.toThrow(/does not implement updateRow/)
|
|
182
|
+
await expect(ctl.deleteRow('1')).rejects.toThrow(/does not implement deleteRow/)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('rejects mutations after dispose, without calling the source', async () => {
|
|
186
|
+
const src = writable()
|
|
187
|
+
const ctl = createServerDataSource(src, { onChange: () => {} })
|
|
188
|
+
ctl.dispose()
|
|
189
|
+
await expect(ctl.deleteRow('0')).rejects.toThrow(/disposed/)
|
|
190
|
+
expect(src.deleteRow).not.toHaveBeenCalled()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('leaves read state untouched when a mutation fails', async () => {
|
|
194
|
+
const src = writable()
|
|
195
|
+
src.createRow.mockRejectedValueOnce(new Error('boom'))
|
|
196
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
197
|
+
ctl.refresh()
|
|
198
|
+
await flush()
|
|
199
|
+
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow('boom')
|
|
200
|
+
expect(ctl.getState().saving).toBe(false)
|
|
201
|
+
expect(ctl.getState().total).toBe(3) // unchanged; no refresh on failure
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('createServerDataSource optimistic mutations', () => {
|
|
206
|
+
type URow = { id: number; name: string }
|
|
207
|
+
const opts = (onChange: (s: any) => void = () => {}) => ({
|
|
208
|
+
pageSize: 50,
|
|
209
|
+
optimistic: true,
|
|
210
|
+
getRowId: (r: URow) => String(r.id),
|
|
211
|
+
onChange,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
function writable() {
|
|
215
|
+
let store: URow[] = [
|
|
216
|
+
{ id: 0, name: 'a' },
|
|
217
|
+
{ id: 1, name: 'b' },
|
|
218
|
+
{ id: 2, name: 'c' },
|
|
219
|
+
]
|
|
220
|
+
return {
|
|
221
|
+
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
222
|
+
rows: store.slice(req.startRow, req.endRow),
|
|
223
|
+
rowCount: store.length,
|
|
224
|
+
})),
|
|
225
|
+
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
226
|
+
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
227
|
+
return store.find((r) => String(r.id) === id)!
|
|
228
|
+
}),
|
|
229
|
+
deleteRow: vi.fn(async (id: string) => {
|
|
230
|
+
store = store.filter((r) => String(r.id) !== id)
|
|
231
|
+
}),
|
|
232
|
+
} as any
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
it('applies an update to local rows immediately, before the server resolves', async () => {
|
|
236
|
+
const src = writable()
|
|
237
|
+
src.updateRow.mockImplementationOnce(
|
|
238
|
+
(id: string, patch: any) => new Promise((r) => setTimeout(() => r({ id: Number(id), ...patch }), 30)),
|
|
239
|
+
)
|
|
240
|
+
const ctl = createServerDataSource(src, opts())
|
|
241
|
+
ctl.refresh()
|
|
242
|
+
await flush()
|
|
243
|
+
const p = ctl.updateRow('1', { name: 'B!' })
|
|
244
|
+
// synchronously reflected, no refetch
|
|
245
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
246
|
+
expect(ctl.getState().saving).toBe(true)
|
|
247
|
+
await p
|
|
248
|
+
expect(ctl.getState().saving).toBe(false)
|
|
249
|
+
// getRows only called once (the initial refresh) - no refetch for the update
|
|
250
|
+
expect(src.getRows).toHaveBeenCalledTimes(1)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('rolls an update back on error', async () => {
|
|
254
|
+
const src = writable()
|
|
255
|
+
src.updateRow.mockRejectedValueOnce(new Error('nope'))
|
|
256
|
+
const ctl = createServerDataSource(src, opts())
|
|
257
|
+
ctl.refresh()
|
|
258
|
+
await flush()
|
|
259
|
+
await expect(ctl.updateRow('1', { name: 'B!' })).rejects.toThrow('nope')
|
|
260
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('b') // restored
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('removes a row immediately on delete and decrements total, restoring on error', async () => {
|
|
264
|
+
const src = writable()
|
|
265
|
+
const ctl = createServerDataSource(src, opts())
|
|
266
|
+
ctl.refresh()
|
|
267
|
+
await flush()
|
|
268
|
+
|
|
269
|
+
const p = ctl.deleteRow('0')
|
|
270
|
+
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
271
|
+
expect(ctl.getState().total).toBe(2)
|
|
272
|
+
await p
|
|
273
|
+
|
|
274
|
+
src.deleteRow.mockRejectedValueOnce(new Error('locked'))
|
|
275
|
+
await expect(ctl.deleteRow('1')).rejects.toThrow('locked')
|
|
276
|
+
expect(ctl.getState().rows.some((r) => r.id === 1)).toBe(true) // restored
|
|
277
|
+
expect(ctl.getState().total).toBe(2)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('falls back to a refetch when the row is not on the current page', async () => {
|
|
281
|
+
const src = writable()
|
|
282
|
+
const ctl = createServerDataSource(src, opts())
|
|
283
|
+
ctl.refresh()
|
|
284
|
+
await flush()
|
|
285
|
+
await ctl.updateRow('999', { name: 'ghost' }) // not present locally
|
|
286
|
+
// fell back to mutate() which refreshes: getRows called again
|
|
287
|
+
expect(src.getRows).toHaveBeenCalledTimes(2)
|
|
288
|
+
})
|
|
289
|
+
})
|