@svgrid/grid 3.0.4 → 3.0.6
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 +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +96 -31
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four seams a server-backed row model plugs into, all free:
|
|
3
|
+
*
|
|
4
|
+
* - `onVisibleRangeChange` - which rows are on screen, by index
|
|
5
|
+
* - `rowPlaceholder` / `onRetryRow` - rows whose data has not arrived
|
|
6
|
+
* - `rowSelectionModel` - selection the grid does not own
|
|
7
|
+
*
|
|
8
|
+
* They are deliberately generic: none of them mentions groups, blocks or a
|
|
9
|
+
* datasource, so the free infinite controller and the Enterprise row model
|
|
10
|
+
* plug into the same four holes, and an app with its own fetching can too.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
13
|
+
import { mount, unmount } from 'svelte'
|
|
14
|
+
import SvGrid from './SvGrid.svelte'
|
|
15
|
+
import {
|
|
16
|
+
createCoreRowModel,
|
|
17
|
+
rowSelectionFeature,
|
|
18
|
+
rowSortingFeature,
|
|
19
|
+
tableFeatures,
|
|
20
|
+
} from './index'
|
|
21
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
22
|
+
|
|
23
|
+
type Row = { id: number; name: string }
|
|
24
|
+
const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
|
|
25
|
+
const cols: ColumnDef<typeof features, Row>[] = [
|
|
26
|
+
{ field: 'name', header: 'Name', width: 160 },
|
|
27
|
+
{ field: 'id', header: 'Id', width: 80 },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
31
|
+
const rowsOf = (n: number): Row[] =>
|
|
32
|
+
Array.from({ length: n }, (_, i) => ({ id: i, name: `Row ${i}` }))
|
|
33
|
+
|
|
34
|
+
function mountGrid(extra: Record<string, unknown>, data: Row[] = rowsOf(5)) {
|
|
35
|
+
return new Promise<{
|
|
36
|
+
api: SvGridApi<typeof features, Row>
|
|
37
|
+
target: HTMLElement
|
|
38
|
+
destroy: () => void
|
|
39
|
+
}>((res, rej) => {
|
|
40
|
+
const target = document.createElement('div')
|
|
41
|
+
document.body.appendChild(target)
|
|
42
|
+
const app = mount(SvGrid, {
|
|
43
|
+
target,
|
|
44
|
+
props: {
|
|
45
|
+
data,
|
|
46
|
+
columns: cols,
|
|
47
|
+
features,
|
|
48
|
+
_rowModels: { coreRowModel: createCoreRowModel() },
|
|
49
|
+
getRowId: (r: Row) => String(r.id),
|
|
50
|
+
containerHeight: 200,
|
|
51
|
+
virtualization: false,
|
|
52
|
+
onApiReady(api: SvGridApi<typeof features, Row>) {
|
|
53
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
54
|
+
},
|
|
55
|
+
...extra,
|
|
56
|
+
} as any,
|
|
57
|
+
})
|
|
58
|
+
queueMicrotask(() => {
|
|
59
|
+
if (!target.querySelector('[role="grid"]')) rej(new Error('no grid'))
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('onVisibleRangeChange', () => {
|
|
65
|
+
it('reports the whole list when virtualization is off', async () => {
|
|
66
|
+
const seen: Array<{ startIndex: number; endIndex: number }> = []
|
|
67
|
+
const { destroy } = await mountGrid({ onVisibleRangeChange: (r: any) => seen.push(r) }, rowsOf(5))
|
|
68
|
+
await tick()
|
|
69
|
+
|
|
70
|
+
expect(seen.at(-1)).toEqual({ startIndex: 0, endIndex: 4 })
|
|
71
|
+
destroy()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('reports row indices, not pixels, when virtualization is on', async () => {
|
|
75
|
+
const seen: Array<{ startIndex: number; endIndex: number }> = []
|
|
76
|
+
const { destroy } = await mountGrid(
|
|
77
|
+
{
|
|
78
|
+
onVisibleRangeChange: (r: any) => seen.push(r),
|
|
79
|
+
virtualization: true,
|
|
80
|
+
containerHeight: 200,
|
|
81
|
+
},
|
|
82
|
+
rowsOf(10_000),
|
|
83
|
+
)
|
|
84
|
+
await tick()
|
|
85
|
+
|
|
86
|
+
const last = seen.at(-1)!
|
|
87
|
+
expect(last.startIndex).toBe(0)
|
|
88
|
+
// A 200px viewport cannot be showing ten thousand rows, and the numbers
|
|
89
|
+
// are row indices - if these were the pixel offsets the old deriveds
|
|
90
|
+
// carry, endIndex would be in the hundreds.
|
|
91
|
+
expect(last.endIndex).toBeGreaterThan(0)
|
|
92
|
+
expect(last.endIndex).toBeLessThan(200)
|
|
93
|
+
destroy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not fire again while the range is unchanged', async () => {
|
|
97
|
+
const seen: Array<unknown> = []
|
|
98
|
+
const { api, destroy } = await mountGrid({ onVisibleRangeChange: (r: any) => seen.push(r) })
|
|
99
|
+
await tick()
|
|
100
|
+
const count = seen.length
|
|
101
|
+
|
|
102
|
+
// Something unrelated changes; the visible rows do not.
|
|
103
|
+
api.selectRows(['1'])
|
|
104
|
+
await tick()
|
|
105
|
+
expect(seen).toHaveLength(count)
|
|
106
|
+
destroy()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('rowPlaceholder', () => {
|
|
111
|
+
it('draws a skeleton cell per column for a loading row', async () => {
|
|
112
|
+
const { target, destroy } = await mountGrid({
|
|
113
|
+
rowPlaceholder: (row: Row) => (row.id === 2 ? 'loading' : null),
|
|
114
|
+
})
|
|
115
|
+
await tick()
|
|
116
|
+
|
|
117
|
+
const placeholder = target.querySelector('.sv-grid-placeholder-row')!
|
|
118
|
+
expect(placeholder).not.toBeNull()
|
|
119
|
+
expect(placeholder.getAttribute('aria-busy')).toBe('true')
|
|
120
|
+
// One shimmer per real column, so the table keeps its shape.
|
|
121
|
+
expect(placeholder.querySelectorAll('.sv-grid-placeholder-skeleton')).toHaveLength(cols.length)
|
|
122
|
+
// And the rows around it are untouched.
|
|
123
|
+
expect(target.querySelectorAll('.sv-grid-placeholder-row')).toHaveLength(1)
|
|
124
|
+
destroy()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('draws one full-width message with a Retry button for a failed row', async () => {
|
|
128
|
+
const retried: Array<{ row: Row; index: number }> = []
|
|
129
|
+
const { target, destroy } = await mountGrid({
|
|
130
|
+
rowPlaceholder: (row: Row) => (row.id === 3 ? 'failed' : null),
|
|
131
|
+
onRetryRow: (row: Row, index: number) => retried.push({ row, index }),
|
|
132
|
+
})
|
|
133
|
+
await tick()
|
|
134
|
+
|
|
135
|
+
const failed = target.querySelector('.sv-grid-placeholder-failed')!
|
|
136
|
+
expect(failed).not.toBeNull()
|
|
137
|
+
expect(failed.textContent).toContain('Could not load')
|
|
138
|
+
expect(failed.querySelectorAll('.sv-grid-placeholder-skeleton')).toHaveLength(0)
|
|
139
|
+
|
|
140
|
+
const retry = failed.querySelector<HTMLButtonElement>('.sv-grid-placeholder-retry')!
|
|
141
|
+
retry.click()
|
|
142
|
+
expect(retried).toHaveLength(1)
|
|
143
|
+
expect(retried[0]!.row.id).toBe(3)
|
|
144
|
+
expect(retried[0]!.index).toBe(3)
|
|
145
|
+
destroy()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('gives a run of failed rows one message, and a band under it', async () => {
|
|
149
|
+
// A failed block is claimed row by row; the message and Retry sit on
|
|
150
|
+
// the first row of the run, the rest are the same tint with nothing
|
|
151
|
+
// to read, so a hundred-row block is not a hundred red lines.
|
|
152
|
+
const { target, destroy } = await mountGrid(
|
|
153
|
+
{
|
|
154
|
+
rowPlaceholder: (row: Row) => (row.id >= 2 && row.id <= 4 ? 'failed' : null),
|
|
155
|
+
onRetryRow: () => {},
|
|
156
|
+
},
|
|
157
|
+
rowsOf(7),
|
|
158
|
+
)
|
|
159
|
+
await tick()
|
|
160
|
+
const failed = [...target.querySelectorAll('.sv-grid-placeholder-failed')]
|
|
161
|
+
expect(failed).toHaveLength(3)
|
|
162
|
+
expect(failed.map((r) => r.classList.contains('sv-grid-placeholder-failed-cont'))).toEqual([false, true, true])
|
|
163
|
+
expect(target.querySelectorAll('.sv-grid-placeholder-retry')).toHaveLength(1)
|
|
164
|
+
expect(failed[0]!.textContent).toContain('Could not load')
|
|
165
|
+
expect(failed[1]!.textContent!.trim()).toBe('')
|
|
166
|
+
// Every row of the run keeps its own slot: the scrollbar does not move.
|
|
167
|
+
expect(target.querySelectorAll('tbody .sv-grid-row').length).toBe(7)
|
|
168
|
+
destroy()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('omits the Retry button when there is no handler to run', async () => {
|
|
172
|
+
const { target, destroy } = await mountGrid({
|
|
173
|
+
rowPlaceholder: (row: Row) => (row.id === 3 ? 'failed' : null),
|
|
174
|
+
})
|
|
175
|
+
await tick()
|
|
176
|
+
expect(target.querySelector('.sv-grid-placeholder-retry')).toBeNull()
|
|
177
|
+
destroy()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('styles footer and grand-total rows a row model flags on the data', async () => {
|
|
181
|
+
// A server-side model cannot reach the row objects the grid builds, so
|
|
182
|
+
// it puts the client model's flags on the data instead; same chrome.
|
|
183
|
+
const data = [
|
|
184
|
+
{ id: 1, name: 'Row 1' },
|
|
185
|
+
{ id: 2, name: 'Total', __groupFooter: true },
|
|
186
|
+
{ id: 3, name: 'Grand total', __groupFooter: true, __grandTotal: true },
|
|
187
|
+
] as Row[]
|
|
188
|
+
const { target, destroy } = await mountGrid({}, data)
|
|
189
|
+
await tick()
|
|
190
|
+
const rows = [...target.querySelectorAll('tbody .sv-grid-row')]
|
|
191
|
+
expect(rows.map((r) => r.classList.contains('sv-grid-group-footer-row'))).toEqual([false, true, true])
|
|
192
|
+
expect(rows.map((r) => r.classList.contains('sv-grid-grand-total-row'))).toEqual([false, false, true])
|
|
193
|
+
destroy()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('keeps placeholder rows out of select-all', async () => {
|
|
197
|
+
const { api, destroy } = await mountGrid({
|
|
198
|
+
showRowSelection: true,
|
|
199
|
+
rowPlaceholder: (row: Row) => (row.id >= 3 ? 'loading' : null),
|
|
200
|
+
})
|
|
201
|
+
await tick()
|
|
202
|
+
|
|
203
|
+
api.selectAllRows()
|
|
204
|
+
await tick()
|
|
205
|
+
// Selecting a row that has not loaded would leave an id that never
|
|
206
|
+
// resolves to anything.
|
|
207
|
+
expect(api.getSelectedRowIds().sort()).toEqual(['0', '1', '2'])
|
|
208
|
+
destroy()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('changes nothing for a grid that does not use it', async () => {
|
|
212
|
+
const { target, destroy } = await mountGrid({})
|
|
213
|
+
await tick()
|
|
214
|
+
expect(target.querySelector('.sv-grid-placeholder-row')).toBeNull()
|
|
215
|
+
expect(target.querySelectorAll('.sv-grid-row').length).toBeGreaterThan(0)
|
|
216
|
+
destroy()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('rowSelectionModel', () => {
|
|
221
|
+
/**
|
|
222
|
+
* "Everything except these", the shape a server-side selection needs.
|
|
223
|
+
*
|
|
224
|
+
* Its state is a rune, so the header checkbox re-derives when the model
|
|
225
|
+
* changes. That is a requirement of the seam, not an accident of the test:
|
|
226
|
+
* the grid re-reads `headerState()` only when something reactive that it
|
|
227
|
+
* touched has changed.
|
|
228
|
+
*/
|
|
229
|
+
function exceptModel(totalRows: number) {
|
|
230
|
+
const state = $state({ selectAll: false, toggled: new Set<string>() })
|
|
231
|
+
return {
|
|
232
|
+
state,
|
|
233
|
+
isSelected: (rowId: string) => state.selectAll !== state.toggled.has(rowId),
|
|
234
|
+
headerState: () => {
|
|
235
|
+
if (state.selectAll) return state.toggled.size === 0 ? 'all' : 'some'
|
|
236
|
+
return state.toggled.size === 0 ? 'none' : 'some'
|
|
237
|
+
},
|
|
238
|
+
toggle: (rowId: string) => {
|
|
239
|
+
const next = new Set(state.toggled)
|
|
240
|
+
if (next.has(rowId)) next.delete(rowId)
|
|
241
|
+
else next.add(rowId)
|
|
242
|
+
state.toggled = next
|
|
243
|
+
},
|
|
244
|
+
toggleAll: (next: boolean) => {
|
|
245
|
+
state.selectAll = next
|
|
246
|
+
state.toggled = new Set()
|
|
247
|
+
},
|
|
248
|
+
selectedCount: () => (state.selectAll ? totalRows - state.toggled.size : state.toggled.size),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
it('answers the header checkbox instead of counting loaded rows', async () => {
|
|
253
|
+
const model = exceptModel(1_000_000)
|
|
254
|
+
const { target, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
|
|
255
|
+
await tick()
|
|
256
|
+
|
|
257
|
+
const header = target.querySelector('thead [role="checkbox"], thead input[type="checkbox"]')!
|
|
258
|
+
expect(header.getAttribute('aria-checked')).toBe('false')
|
|
259
|
+
|
|
260
|
+
model.toggleAll(true)
|
|
261
|
+
await vi.waitFor(() =>
|
|
262
|
+
expect(
|
|
263
|
+
target.querySelector('thead [role="checkbox"], thead input[type="checkbox"]')!
|
|
264
|
+
.getAttribute('aria-checked'),
|
|
265
|
+
).toBe('true'),
|
|
266
|
+
)
|
|
267
|
+
// Five rows are loaded; a million are selected. The grid says "all"
|
|
268
|
+
// because the model said so, which is the whole point of the seam.
|
|
269
|
+
expect(model.selectedCount()).toBe(1_000_000)
|
|
270
|
+
destroy()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('routes the header checkbox click to toggleAll, not to the loaded rows', async () => {
|
|
274
|
+
const model = exceptModel(1_000_000)
|
|
275
|
+
const { target, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
|
|
276
|
+
await tick()
|
|
277
|
+
|
|
278
|
+
const header = target.querySelector<HTMLElement>('thead [role="checkbox"], thead input[type="checkbox"]')!
|
|
279
|
+
header.click()
|
|
280
|
+
await tick()
|
|
281
|
+
|
|
282
|
+
expect(model.state.selectAll).toBe(true)
|
|
283
|
+
expect(model.selectedCount()).toBe(1_000_000)
|
|
284
|
+
destroy()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('routes a row checkbox through the model', async () => {
|
|
288
|
+
const model = exceptModel(1_000_000)
|
|
289
|
+
const { api, destroy } = await mountGrid({ showRowSelection: true, rowSelectionModel: model })
|
|
290
|
+
await tick()
|
|
291
|
+
|
|
292
|
+
api.toggleRowSelected('2')
|
|
293
|
+
await tick()
|
|
294
|
+
expect(model.state.toggled.has('2')).toBe(true)
|
|
295
|
+
expect(model.isSelected('2')).toBe(true)
|
|
296
|
+
|
|
297
|
+
model.toggleAll(true)
|
|
298
|
+
await tick()
|
|
299
|
+
api.toggleRowSelected('2')
|
|
300
|
+
// Under select-all, toggling a row means "except this one".
|
|
301
|
+
expect(model.isSelected('2')).toBe(false)
|
|
302
|
+
expect(model.selectedCount()).toBe(999_999)
|
|
303
|
+
destroy()
|
|
304
|
+
})
|
|
305
|
+
})
|
|
@@ -40,7 +40,7 @@ const rows: Row[] = [
|
|
|
40
40
|
const cols: ColumnDef<typeof features, Row>[] = [
|
|
41
41
|
{ field: 'id', header: 'ID', width: 100, editable: false },
|
|
42
42
|
{ field: 'account', header: 'Account', width: 200, editable: false },
|
|
43
|
-
{ field: 'arr', header: 'ARR', width: 130, editable: false, align: 'right',
|
|
43
|
+
{ field: 'arr', header: 'ARR', width: 130, editable: false, align: 'right', cellClass: 'money',
|
|
44
44
|
format: { type: 'number', options: { style: 'currency', currency: 'USD', maximumFractionDigits: 0 } } },
|
|
45
45
|
{ field: 'seats', header: 'Seats', width: 90, editable: false, align: 'right' },
|
|
46
46
|
]
|
|
@@ -167,6 +167,20 @@ describe('SvGrid - pinnedTopRows render', () => {
|
|
|
167
167
|
} finally { destroy() }
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
it('aligns and classes the cells the way the column says (a total sat left under a right-aligned column)', async () => {
|
|
171
|
+
const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] })
|
|
172
|
+
try {
|
|
173
|
+
await tick()
|
|
174
|
+
const tds = target.querySelectorAll('tr.sv-grid-pinned-row-top td')
|
|
175
|
+
expect(tds[0]!.getAttribute('data-align')).toBe('left')
|
|
176
|
+
expect(tds[2]!.getAttribute('data-align')).toBe('right')
|
|
177
|
+
expect(tds[3]!.getAttribute('data-align')).toBe('right')
|
|
178
|
+
// A static cellClass reaches the pinned cell; only a function did before.
|
|
179
|
+
expect(tds[2]!.classList.contains('money')).toBe(true)
|
|
180
|
+
expect(tds[0]!.classList.contains('money')).toBe(false)
|
|
181
|
+
} finally { destroy() }
|
|
182
|
+
})
|
|
183
|
+
|
|
170
184
|
it('stacks multiple pinned-top rows with data-pinned-index', async () => {
|
|
171
185
|
const { target, destroy } = await mountGrid({
|
|
172
186
|
pinnedTopRows: [TOTALS, BENCHMARK],
|
|
@@ -87,7 +87,7 @@ describe('selectionBar - without the Enterprise renderer', () => {
|
|
|
87
87
|
it('says a license is needed, and links to it', async () => {
|
|
88
88
|
// Naming the package is what unblocks a developer trying it; saying a key
|
|
89
89
|
// is needed is what they have to know before shipping. Enterprise is
|
|
90
|
-
//
|
|
90
|
+
// runs without a key, so the wording must not imply the feature is inert without
|
|
91
91
|
// one - it says "watermark", which is what actually happens.
|
|
92
92
|
const { api, target, destroy } = await mountGrid(true)
|
|
93
93
|
await tick()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky group rows: the group a row belongs to stays under the header
|
|
3
|
+
* while its rows scroll past. Under virtualization the band renders a copy
|
|
4
|
+
* of each ancestor; without it the real rows stick. Driven through the
|
|
5
|
+
* `serverGroup` seam with plain data, so no row model is needed to say
|
|
6
|
+
* which rows are groups and how deep they sit.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import { mount, unmount } from 'svelte'
|
|
10
|
+
import SvGrid from './SvGrid.svelte'
|
|
11
|
+
import { createCoreRowModel, tableFeatures } from './index'
|
|
12
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
13
|
+
|
|
14
|
+
type Row = { id: string; name: string; kind: 'group' | 'leaf'; level: number }
|
|
15
|
+
const features = tableFeatures({})
|
|
16
|
+
const cols: ColumnDef<typeof features, Row>[] = [
|
|
17
|
+
{ field: 'name', header: 'Name', width: 200 },
|
|
18
|
+
{ field: 'id', header: 'Id', width: 80 },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
22
|
+
|
|
23
|
+
/** A(0) > A1(1) > 20 leaves, A2(1) > 20 leaves, B(0) > B1(1) > 20 leaves: 65 rows. */
|
|
24
|
+
function tree(): Row[] {
|
|
25
|
+
const out: Row[] = []
|
|
26
|
+
let n = 0
|
|
27
|
+
const leaves = (under: string) => {
|
|
28
|
+
for (let i = 0; i < 20; i += 1) out.push({ id: `${under}-${i}`, name: `leaf ${n++}`, kind: 'leaf', level: 2 })
|
|
29
|
+
}
|
|
30
|
+
out.push({ id: 'A', name: 'A', kind: 'group', level: 0 })
|
|
31
|
+
out.push({ id: 'A1', name: 'A1', kind: 'group', level: 1 })
|
|
32
|
+
leaves('A1')
|
|
33
|
+
out.push({ id: 'A2', name: 'A2', kind: 'group', level: 1 })
|
|
34
|
+
leaves('A2')
|
|
35
|
+
out.push({ id: 'B', name: 'B', kind: 'group', level: 0 })
|
|
36
|
+
out.push({ id: 'B1', name: 'B1', kind: 'group', level: 1 })
|
|
37
|
+
leaves('B1')
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mountGrid(extra: Record<string, unknown>) {
|
|
42
|
+
return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>((res, rej) => {
|
|
43
|
+
const target = document.createElement('div')
|
|
44
|
+
document.body.appendChild(target)
|
|
45
|
+
const app = mount(SvGrid, {
|
|
46
|
+
target,
|
|
47
|
+
props: {
|
|
48
|
+
data: tree(),
|
|
49
|
+
columns: cols,
|
|
50
|
+
features,
|
|
51
|
+
_rowModels: { coreRowModel: createCoreRowModel() },
|
|
52
|
+
getRowId: (r: Row) => r.id,
|
|
53
|
+
rowHeight: 30,
|
|
54
|
+
containerHeight: 200,
|
|
55
|
+
stickyGroupRows: true,
|
|
56
|
+
serverGroup: {
|
|
57
|
+
isGroup: (r: Row) => r.kind === 'group',
|
|
58
|
+
level: (r: Row) => r.level,
|
|
59
|
+
expanded: () => true,
|
|
60
|
+
onToggle: () => {},
|
|
61
|
+
},
|
|
62
|
+
onApiReady(api: SvGridApi<typeof features, Row>) {
|
|
63
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
64
|
+
},
|
|
65
|
+
...extra,
|
|
66
|
+
} as any,
|
|
67
|
+
})
|
|
68
|
+
queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function scrollTo(target: HTMLElement, top: number) {
|
|
73
|
+
const container = target.querySelector('.sv-grid-container') as HTMLElement
|
|
74
|
+
container.scrollTop = top
|
|
75
|
+
container.dispatchEvent(new Event('scroll'))
|
|
76
|
+
// The virtualizer syncs to the scroll position on the next animation frame.
|
|
77
|
+
await new Promise<void>((r) => requestAnimationFrame(() => r()))
|
|
78
|
+
await tick()
|
|
79
|
+
await tick()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const stickyNames = (target: HTMLElement) =>
|
|
83
|
+
[...target.querySelectorAll<HTMLElement>('tr.sv-grid-row-sticky-group')].map((tr) => tr.textContent!.replace(/\s+/g, ' ').trim().split(' ')[0])
|
|
84
|
+
|
|
85
|
+
describe('stickyGroupRows under virtualization', () => {
|
|
86
|
+
it('renders copies of the ancestors of the first row under the header, deepest last', async () => {
|
|
87
|
+
const { target, destroy } = await mountGrid({ virtualization: true })
|
|
88
|
+
try {
|
|
89
|
+
await tick()
|
|
90
|
+
// Row 0 is A itself: nothing to hold.
|
|
91
|
+
expect(stickyNames(target)).toEqual([])
|
|
92
|
+
// Row 10 is a leaf under A1: A, then A1.
|
|
93
|
+
await scrollTo(target, 300)
|
|
94
|
+
expect(stickyNames(target)).toEqual(['A', 'A1'])
|
|
95
|
+
const band = [...target.querySelectorAll<HTMLElement>('tr.sv-grid-row-sticky-group')]
|
|
96
|
+
expect(band[0]!.getAttribute('aria-hidden')).toBe('true')
|
|
97
|
+
expect(band[1]!.classList.contains('sv-grid-row-sticky-group-last')).toBe(true)
|
|
98
|
+
// The second sits a row under the first.
|
|
99
|
+
expect(parseInt(band[1]!.style.top, 10) - parseInt(band[0]!.style.top, 10)).toBe(30)
|
|
100
|
+
// A copy carries no cell ids, so the real row's id stays unique.
|
|
101
|
+
expect(band[0]!.querySelector('[id]')).toBeNull()
|
|
102
|
+
// Row 30 is a leaf under A2.
|
|
103
|
+
await scrollTo(target, 900)
|
|
104
|
+
expect(stickyNames(target)).toEqual(['A', 'A2'])
|
|
105
|
+
// Row 50 is a leaf under B1.
|
|
106
|
+
await scrollTo(target, 1500)
|
|
107
|
+
expect(stickyNames(target)).toEqual(['B', 'B1'])
|
|
108
|
+
// Back to the top: the band is gone.
|
|
109
|
+
await scrollTo(target, 0)
|
|
110
|
+
expect(stickyNames(target)).toEqual([])
|
|
111
|
+
} finally {
|
|
112
|
+
destroy()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('is off unless asked for', async () => {
|
|
117
|
+
const { target, destroy } = await mountGrid({ virtualization: true, stickyGroupRows: false })
|
|
118
|
+
try {
|
|
119
|
+
await scrollTo(target, 300)
|
|
120
|
+
expect(stickyNames(target)).toEqual([])
|
|
121
|
+
} finally {
|
|
122
|
+
destroy()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('stickyGroupRows without virtualization', () => {
|
|
128
|
+
it('makes the real ancestor rows sticky instead of copying them', async () => {
|
|
129
|
+
const { target, destroy } = await mountGrid({ virtualization: false })
|
|
130
|
+
try {
|
|
131
|
+
await scrollTo(target, 300)
|
|
132
|
+
const sticky = [...target.querySelectorAll<HTMLElement>('tr.sv-grid-row-sticky-group')]
|
|
133
|
+
expect(sticky.map((tr) => tr.textContent!.trim().split(/\s+/)[0])).toEqual(['A', 'A1'])
|
|
134
|
+
// They are the rows themselves: cells keep their row index.
|
|
135
|
+
expect(sticky[0]!.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row')).toBe('0')
|
|
136
|
+
expect(sticky[1]!.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row')).toBe('1')
|
|
137
|
+
expect(sticky[0]!.getAttribute('aria-hidden')).toBeNull()
|
|
138
|
+
// Every row renders exactly once.
|
|
139
|
+
const rows = [...target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row')]
|
|
140
|
+
expect(rows).toHaveLength(65)
|
|
141
|
+
} finally {
|
|
142
|
+
destroy()
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every Enterprise gate carries the same licensing line.
|
|
3
3
|
*
|
|
4
|
-
* There are
|
|
5
|
-
* selection bar - and they used to be four hand-written paragraphs.
|
|
6
|
-
* worded
|
|
4
|
+
* There are five of them - Kanban board, scheduler, Gantt, pivot mode and the
|
|
5
|
+
* selection bar - and they used to be four hand-written paragraphs. Gates
|
|
6
|
+
* worded one way each is how a product ends up explaining its own licensing
|
|
7
7
|
* inconsistently, so they now share one snippet. This is the test that keeps
|
|
8
|
-
* them sharing it: a
|
|
8
|
+
* them sharing it: a sixth Enterprise feature that hand-rolls its own note
|
|
9
9
|
* will show up here as a missing licensing line.
|
|
10
10
|
*
|
|
11
11
|
* None of the renderers are registered in this file, which is the point - it
|
|
@@ -17,6 +17,7 @@ import SvGrid from './SvGrid.svelte'
|
|
|
17
17
|
import {
|
|
18
18
|
createCoreRowModel,
|
|
19
19
|
hasBoardView,
|
|
20
|
+
hasGanttView,
|
|
20
21
|
hasSchedulerView,
|
|
21
22
|
hasSelectionBarView,
|
|
22
23
|
tableFeatures,
|
|
@@ -69,12 +70,14 @@ describe('Enterprise upsell notes', () => {
|
|
|
69
70
|
// Otherwise every assertion below is checking the wrong branch.
|
|
70
71
|
expect(hasBoardView()).toBe(false)
|
|
71
72
|
expect(hasSchedulerView()).toBe(false)
|
|
73
|
+
expect(hasGanttView()).toBe(false)
|
|
72
74
|
expect(hasSelectionBarView()).toBe(false)
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
const gates: Array<[string, Record<string, unknown>, ((api: SvGridApi<typeof features, Row>) => void)?]> = [
|
|
76
78
|
['Kanban board', { board: { groupBy: 'team' } }],
|
|
77
79
|
['scheduler', { scheduler: { startField: 'name' } }],
|
|
80
|
+
['Gantt', { gantt: { startField: 'name' } }],
|
|
78
81
|
['selection bar', { showRowSelection: true, selectionBar: true }, (api) => api.selectRows(['1'])],
|
|
79
82
|
// pivotViewOn = a pivot config AND pivot mode on; without the Pro engine
|
|
80
83
|
// `pivotResult` stays null and the upsell renders in its place.
|
|
@@ -92,7 +95,7 @@ describe('Enterprise upsell notes', () => {
|
|
|
92
95
|
})
|
|
93
96
|
|
|
94
97
|
const note = target.querySelector(LICENSE)!
|
|
95
|
-
//
|
|
98
|
+
// The feature runs without a key and the grid watermarks.
|
|
96
99
|
// The wording must not imply it is inert, because it is not.
|
|
97
100
|
expect(note.textContent, name).toContain('license key')
|
|
98
101
|
expect(note.textContent, name).toContain('watermark')
|
package/src/test-setup.ts
CHANGED
|
@@ -59,4 +59,16 @@ if (typeof Element !== 'undefined' && !Element.prototype.animate) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// jsdom has no canvas: getContext() returns null and prints "Not implemented:
|
|
63
|
+
// HTMLCanvasElement's getContext() method" through its virtual console on
|
|
64
|
+
// every call, hundreds of lines per run that bury the real warnings. The
|
|
65
|
+
// callers (the sheet's text measuring, the picture rasteriser) already treat a
|
|
66
|
+
// null context as "no canvas", so answering null quietly changes nothing a
|
|
67
|
+
// test can see; a test that needs a context still spies on the prototype.
|
|
68
|
+
if (typeof HTMLCanvasElement !== 'undefined') {
|
|
69
|
+
;(HTMLCanvasElement.prototype as any).getContext = function () {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
export {}
|
package/src/transaction.test.ts
CHANGED
|
@@ -97,4 +97,62 @@ describe('api.applyTransaction', () => {
|
|
|
97
97
|
destroy()
|
|
98
98
|
}
|
|
99
99
|
})
|
|
100
|
+
it('removes a row that the same transaction also updates, by id or by reference', async () => {
|
|
101
|
+
const { api, destroy } = await mountGrid()
|
|
102
|
+
try {
|
|
103
|
+
const byId = api.applyTransaction({ update: [{ id: 'a', symbol: 'AAPL', price: 1 }], remove: ['a'] })
|
|
104
|
+
await tick()
|
|
105
|
+
expect(byId).toEqual({ added: 0, updated: 1, removed: 1 })
|
|
106
|
+
expect(api.getData().map((o) => o.id)).toEqual(['b', 'c'])
|
|
107
|
+
const ref = api.getData()[0]! // 'b'
|
|
108
|
+
const byRef = api.applyTransaction({ update: [{ id: 'b', symbol: 'MSFT', price: 2 }], remove: [ref] })
|
|
109
|
+
await tick()
|
|
110
|
+
expect(byRef).toEqual({ added: 0, updated: 1, removed: 1 })
|
|
111
|
+
expect(api.getData().map((o) => o.id)).toEqual(['c'])
|
|
112
|
+
} finally {
|
|
113
|
+
destroy()
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('an update batch looks its rows up by id instead of walking the array', async () => {
|
|
118
|
+
// The id -> index map is built once per data array and carried across
|
|
119
|
+
// update-only transactions, so the second tick calls getRowId only for
|
|
120
|
+
// the rows in the batch. Before, every transaction called it for every
|
|
121
|
+
// row - 100k calls per tick on a 100k-row feed.
|
|
122
|
+
const target = document.createElement('div')
|
|
123
|
+
document.body.appendChild(target)
|
|
124
|
+
let calls = 0
|
|
125
|
+
let captured: SvGridApi<typeof features, Order> | null = null
|
|
126
|
+
const app = mount(SvGrid, {
|
|
127
|
+
target,
|
|
128
|
+
props: {
|
|
129
|
+
data: seed,
|
|
130
|
+
columns,
|
|
131
|
+
features,
|
|
132
|
+
getRowId: (o: Order) => { calls++; return o.id },
|
|
133
|
+
_rowModels: { coreRowModel: createCoreRowModel(), filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(sortFns) },
|
|
134
|
+
containerHeight: 400,
|
|
135
|
+
virtualization: false,
|
|
136
|
+
onApiReady(api: SvGridApi<typeof features, Order>) { captured = api },
|
|
137
|
+
} as any,
|
|
138
|
+
})
|
|
139
|
+
try {
|
|
140
|
+
await tick()
|
|
141
|
+
const api = captured!
|
|
142
|
+
api.applyTransaction({ update: [{ id: 'b', symbol: 'MSFT', price: 201 }] })
|
|
143
|
+
await tick()
|
|
144
|
+
const afterFirst = calls
|
|
145
|
+
api.applyTransaction({ update: [{ id: 'b', symbol: 'MSFT', price: 202 }] })
|
|
146
|
+
await tick()
|
|
147
|
+
// One call: the id of the update itself. The row model's own getRowId
|
|
148
|
+
// calls for the replaced row are not part of this count because the
|
|
149
|
+
// map answered the lookup.
|
|
150
|
+
expect(calls - afterFirst).toBeLessThanOrEqual(2)
|
|
151
|
+
expect(api.getData()[1]!.price).toBe(202)
|
|
152
|
+
} finally {
|
|
153
|
+
unmount(app)
|
|
154
|
+
target.remove()
|
|
155
|
+
}
|
|
156
|
+
})
|
|
100
157
|
})
|
|
158
|
+
|
package/src/validate.test.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, it } from 'vitest'
|
|
7
7
|
import { validateGridConfig } from './validate'
|
|
8
|
+
import { createRowPlaceholder } from './server-block-cache'
|
|
8
9
|
|
|
9
10
|
type Row = { name: string; amount: number }
|
|
10
11
|
const data: Row[] = [
|
|
@@ -48,6 +49,15 @@ describe('unknown field', () => {
|
|
|
48
49
|
expect(run({ columns: [{ id: 'actions', header: '' }] })).toEqual([])
|
|
49
50
|
})
|
|
50
51
|
|
|
52
|
+
it('looks past placeholder rows, which a row model shows before its data lands', () => {
|
|
53
|
+
const loading = [createRowPlaceholder('loading', { index: 0 }), createRowPlaceholder('loading', { index: 1 })]
|
|
54
|
+
// Only placeholders: nothing to judge the columns by, so no warning.
|
|
55
|
+
expect(run({ data: loading })).toEqual([])
|
|
56
|
+
// Placeholders ahead of real rows: the real rows are the sample.
|
|
57
|
+
expect(run({ data: [...loading, ...data] })).toEqual([])
|
|
58
|
+
expect(run({ data: [...loading, ...data], columns: [{ field: 'naem' }] })).toHaveLength(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
51
61
|
it('looks through group columns', () => {
|
|
52
62
|
const msgs = run({ columns: [{ header: 'Group', columns: [{ field: 'nope' }] }] })
|
|
53
63
|
expect(msgs[0]).toContain('"nope" does not exist')
|
|
@@ -151,6 +161,25 @@ describe('treeData fields', () => {
|
|
|
151
161
|
})
|
|
152
162
|
})
|
|
153
163
|
|
|
164
|
+
describe('gantt versus treeData', () => {
|
|
165
|
+
it('flags both trees being configured at once', () => {
|
|
166
|
+
const msg = run({ gantt: { startField: 'name' }, treeData: { parentField: 'name' } }).find(
|
|
167
|
+
(m) => m.includes('`gantt` and `treeData`'),
|
|
168
|
+
)!
|
|
169
|
+
expect(msg).toBeDefined()
|
|
170
|
+
expect(msg).toContain('gantt.parentField')
|
|
171
|
+
expect(msg).toContain('summary bars will under-report')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('is silent for a Gantt on its own', () => {
|
|
175
|
+
expect(run({ gantt: { startField: 'name' } })).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('is silent for treeData on its own', () => {
|
|
179
|
+
expect(run({ treeData: { parentField: 'name' } })).toEqual([])
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
154
183
|
describe('pinning versus column virtualization', () => {
|
|
155
184
|
it('flags pinning while column virtualization is on by default', () => {
|
|
156
185
|
const msg = run({ initialColumnPinning: { left: ['name'] } })[0]!
|