@svgrid/grid 1.0.0
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/LICENSE +33 -0
- package/README.md +39 -0
- package/dist/FlexRender.svelte +96 -0
- package/dist/FlexRender.svelte.d.ts +49 -0
- package/dist/SvGrid.svelte +8742 -0
- package/dist/SvGrid.svelte.d.ts +381 -0
- package/dist/SvGridChart.svelte +653 -0
- package/dist/SvGridChart.svelte.d.ts +25 -0
- package/dist/SvGridChart.test.d.ts +1 -0
- package/dist/SvGridChart.test.js +62 -0
- package/dist/SvGridDropdown.svelte +675 -0
- package/dist/SvGridDropdown.svelte.d.ts +40 -0
- package/dist/a11y.contract.test.d.ts +1 -0
- package/dist/a11y.contract.test.js +39 -0
- package/dist/a11y.d.ts +38 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.test.d.ts +1 -0
- package/dist/a11y.test.js +44 -0
- package/dist/cell-formatting.d.ts +17 -0
- package/dist/cell-formatting.js +141 -0
- package/dist/cell-formatting.test.d.ts +1 -0
- package/dist/cell-formatting.test.js +234 -0
- package/dist/chart-export.d.ts +14 -0
- package/dist/chart-export.js +112 -0
- package/dist/chart.d.ts +226 -0
- package/dist/chart.js +732 -0
- package/dist/chart.test.d.ts +1 -0
- package/dist/chart.test.js +289 -0
- package/dist/collaboration.d.ts +74 -0
- package/dist/collaboration.js +98 -0
- package/dist/collaboration.test.d.ts +1 -0
- package/dist/collaboration.test.js +65 -0
- package/dist/conditional-formatting.d.ts +91 -0
- package/dist/conditional-formatting.js +170 -0
- package/dist/conditional-formatting.test.d.ts +1 -0
- package/dist/conditional-formatting.test.js +87 -0
- package/dist/core.coverage.test.d.ts +1 -0
- package/dist/core.coverage.test.js +186 -0
- package/dist/core.d.ts +370 -0
- package/dist/core.js +568 -0
- package/dist/core.performance.test.d.ts +1 -0
- package/dist/core.performance.test.js +29 -0
- package/dist/createGrid.svelte.d.ts +6 -0
- package/dist/createGrid.svelte.js +17 -0
- package/dist/createGrid.test.d.ts +1 -0
- package/dist/createGrid.test.js +9 -0
- package/dist/createGridState.svelte.d.ts +3 -0
- package/dist/createGridState.svelte.js +13 -0
- package/dist/editors/cell-editors.d.ts +23 -0
- package/dist/editors/cell-editors.js +97 -0
- package/dist/editors/cell-editors.test.d.ts +1 -0
- package/dist/editors/cell-editors.test.js +75 -0
- package/dist/fill-patterns.d.ts +30 -0
- package/dist/fill-patterns.js +207 -0
- package/dist/fill-patterns.test.d.ts +1 -0
- package/dist/fill-patterns.test.js +81 -0
- package/dist/filtering/excel-filters.d.ts +26 -0
- package/dist/filtering/excel-filters.js +64 -0
- package/dist/filtering/excel-filters.test.d.ts +1 -0
- package/dist/filtering/excel-filters.test.js +116 -0
- package/dist/filtering/locale-filter.test.d.ts +13 -0
- package/dist/filtering/locale-filter.test.js +189 -0
- package/dist/flex-render.d.ts +1 -0
- package/dist/flex-render.js +2 -0
- package/dist/flex-render.test.d.ts +1 -0
- package/dist/flex-render.test.js +142 -0
- package/dist/group-aggregate.test.d.ts +1 -0
- package/dist/group-aggregate.test.js +32 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/keyboard.d.ts +8 -0
- package/dist/keyboard.js +91 -0
- package/dist/keyboard.test.d.ts +1 -0
- package/dist/keyboard.test.js +27 -0
- package/dist/merge-objects.d.ts +9 -0
- package/dist/merge-objects.js +28 -0
- package/dist/named-views.d.ts +42 -0
- package/dist/named-views.js +80 -0
- package/dist/named-views.test.d.ts +1 -0
- package/dist/named-views.test.js +57 -0
- package/dist/new-features.test.d.ts +1 -0
- package/dist/new-features.test.js +183 -0
- package/dist/render-component.d.ts +13 -0
- package/dist/render-component.js +14 -0
- package/dist/render-component.test.d.ts +1 -0
- package/dist/render-component.test.js +41 -0
- package/dist/server-data-source.d.ts +76 -0
- package/dist/server-data-source.js +83 -0
- package/dist/server-data-source.test.d.ts +1 -0
- package/dist/server-data-source.test.js +81 -0
- package/dist/sparkline.d.ts +56 -0
- package/dist/sparkline.js +97 -0
- package/dist/sparkline.test.d.ts +1 -0
- package/dist/sparkline.test.js +52 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +1 -0
- package/dist/subscribe.d.ts +9 -0
- package/dist/subscribe.js +31 -0
- package/dist/subscribe.test.d.ts +1 -0
- package/dist/subscribe.test.js +93 -0
- package/dist/sv-grid-scrollbar.d.ts +1 -0
- package/dist/sv-grid-scrollbar.js +310 -0
- package/dist/svgrid-wrapper.types.d.ts +341 -0
- package/dist/svgrid-wrapper.types.js +1 -0
- package/dist/svgrid.api-extensions.test.d.ts +14 -0
- package/dist/svgrid.api-extensions.test.js +334 -0
- package/dist/svgrid.api.test.d.ts +15 -0
- package/dist/svgrid.api.test.js +169 -0
- package/dist/svgrid.behavior.test.d.ts +15 -0
- package/dist/svgrid.behavior.test.js +605 -0
- package/dist/svgrid.column-reorder.test.d.ts +15 -0
- package/dist/svgrid.column-reorder.test.js +224 -0
- package/dist/svgrid.features.test.d.ts +1 -0
- package/dist/svgrid.features.test.js +129 -0
- package/dist/svgrid.interaction.test.d.ts +9 -0
- package/dist/svgrid.interaction.test.js +318 -0
- package/dist/svgrid.locale-filtering.test.d.ts +15 -0
- package/dist/svgrid.locale-filtering.test.js +261 -0
- package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
- package/dist/svgrid.new-features.wrapper.test.js +136 -0
- package/dist/svgrid.row-pinning.test.d.ts +14 -0
- package/dist/svgrid.row-pinning.test.js +250 -0
- package/dist/svgrid.set-filter.test.d.ts +17 -0
- package/dist/svgrid.set-filter.test.js +268 -0
- package/dist/svgrid.wrapper.test.d.ts +1 -0
- package/dist/svgrid.wrapper.test.js +33 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +29 -0
- package/dist/transaction.test.d.ts +1 -0
- package/dist/transaction.test.js +89 -0
- package/dist/virtualization/column-virtualizer.d.ts +20 -0
- package/dist/virtualization/column-virtualizer.js +19 -0
- package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
- package/dist/virtualization/column-virtualizer.test.js +25 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
- package/dist/virtualization/types.d.ts +28 -0
- package/dist/virtualization/types.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +13 -0
- package/dist/virtualization/virtualizer.js +232 -0
- package/dist/virtualization/virtualizer.test.d.ts +1 -0
- package/dist/virtualization/virtualizer.test.js +41 -0
- package/package.json +69 -0
- package/src/FlexRender.svelte +96 -0
- package/src/SvGrid.svelte +8742 -0
- package/src/SvGridChart.svelte +653 -0
- package/src/SvGridChart.test.ts +69 -0
- package/src/SvGridDropdown.svelte +675 -0
- package/src/a11y.contract.test.ts +49 -0
- package/src/a11y.test.ts +59 -0
- package/src/a11y.ts +59 -0
- package/src/cell-formatting.test.ts +273 -0
- package/src/cell-formatting.ts +169 -0
- package/src/chart-export.ts +144 -0
- package/src/chart.test.ts +311 -0
- package/src/chart.ts +934 -0
- package/src/collaboration.test.ts +74 -0
- package/src/collaboration.ts +161 -0
- package/src/conditional-formatting.test.ts +107 -0
- package/src/conditional-formatting.ts +239 -0
- package/src/core.coverage.test.ts +230 -0
- package/src/core.performance.test.ts +30 -0
- package/src/core.ts +991 -0
- package/src/createGrid.svelte.ts +42 -0
- package/src/createGrid.test.ts +10 -0
- package/src/createGridState.svelte.ts +17 -0
- package/src/editors/cell-editors.test.ts +80 -0
- package/src/editors/cell-editors.ts +127 -0
- package/src/fill-patterns.test.ts +93 -0
- package/src/fill-patterns.ts +229 -0
- package/src/filtering/excel-filters.test.ts +131 -0
- package/src/filtering/excel-filters.ts +101 -0
- package/src/filtering/locale-filter.test.ts +220 -0
- package/src/flex-render.test.ts +143 -0
- package/src/flex-render.ts +3 -0
- package/src/group-aggregate.test.ts +48 -0
- package/src/index.ts +159 -0
- package/src/keyboard.test.ts +59 -0
- package/src/keyboard.ts +97 -0
- package/src/merge-objects.ts +48 -0
- package/src/named-views.test.ts +66 -0
- package/src/named-views.ts +120 -0
- package/src/new-features.test.ts +217 -0
- package/src/render-component.test.ts +51 -0
- package/src/render-component.ts +28 -0
- package/src/server-data-source.test.ts +88 -0
- package/src/server-data-source.ts +163 -0
- package/src/sparkline.test.ts +59 -0
- package/src/sparkline.ts +164 -0
- package/src/static-functions.ts +11 -0
- package/src/subscribe.test.ts +103 -0
- package/src/subscribe.ts +38 -0
- package/src/sv-grid-scrollbar.ts +347 -0
- package/src/svgrid-wrapper.types.ts +382 -0
- package/src/svgrid.api-extensions.test.ts +362 -0
- package/src/svgrid.api.test.ts +192 -0
- package/src/svgrid.behavior.test.ts +657 -0
- package/src/svgrid.column-reorder.test.ts +234 -0
- package/src/svgrid.features.test.ts +157 -0
- package/src/svgrid.interaction.test.ts +355 -0
- package/src/svgrid.locale-filtering.test.ts +259 -0
- package/src/svgrid.new-features.wrapper.test.ts +164 -0
- package/src/svgrid.row-pinning.test.ts +266 -0
- package/src/svgrid.set-filter.test.ts +270 -0
- package/src/svgrid.wrapper.test.ts +35 -0
- package/src/test-setup.ts +37 -0
- package/src/transaction.test.ts +100 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -0
- package/src/virtualization/column-virtualizer.ts +30 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
- package/src/virtualization/types.ts +30 -0
- package/src/virtualization/virtualizer.test.ts +47 -0
- package/src/virtualization/virtualizer.ts +270 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for `api.setFacetFilter()` - the imperative entry
|
|
3
|
+
* point that powers the three set-filter UX patterns (Excel-style
|
|
4
|
+
* column-menu, async value loaders, and tree-list cascades). The grid
|
|
5
|
+
* is filter-source-agnostic; this test exercises the API directly so
|
|
6
|
+
* coverage is independent of any specific UI implementation.
|
|
7
|
+
*
|
|
8
|
+
* Covers:
|
|
9
|
+
* - Single column facet filter narrows the displayed rows
|
|
10
|
+
* - Multi-value filter (OR within column) accepts every listed value
|
|
11
|
+
* - Passing null / empty clears the filter
|
|
12
|
+
* - Two columns AND across (different columns)
|
|
13
|
+
* - Combines with operator filter from setFilter()
|
|
14
|
+
* - Survives unknown values (no crash, just no matches)
|
|
15
|
+
* - Unicode / non-ASCII values match exactly (no normalization)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, it } from 'vitest'
|
|
19
|
+
import { mount, unmount } from 'svelte'
|
|
20
|
+
import SvGrid from './SvGrid.svelte'
|
|
21
|
+
import {
|
|
22
|
+
columnFilteringFeature,
|
|
23
|
+
createCoreRowModel,
|
|
24
|
+
createFilteredRowModel,
|
|
25
|
+
createSortedRowModel,
|
|
26
|
+
rowSortingFeature,
|
|
27
|
+
sortFns,
|
|
28
|
+
tableFeatures,
|
|
29
|
+
} from './index'
|
|
30
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
31
|
+
|
|
32
|
+
type Order = {
|
|
33
|
+
id: string
|
|
34
|
+
region: 'Americas' | 'EMEA' | 'APAC'
|
|
35
|
+
country: string
|
|
36
|
+
status: 'open' | 'paid' | 'shipped' | 'delivered' | 'returned'
|
|
37
|
+
amount: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const features = tableFeatures({ columnFilteringFeature, rowSortingFeature })
|
|
41
|
+
|
|
42
|
+
const rows: Order[] = [
|
|
43
|
+
{ id: 'O-1', region: 'Americas', country: 'United States', status: 'open', amount: 120 },
|
|
44
|
+
{ id: 'O-2', region: 'Americas', country: 'United States', status: 'paid', amount: 240 },
|
|
45
|
+
{ id: 'O-3', region: 'Americas', country: 'Canada', status: 'delivered', amount: 410 },
|
|
46
|
+
{ id: 'O-4', region: 'Americas', country: 'Brazil', status: 'shipped', amount: 180 },
|
|
47
|
+
{ id: 'O-5', region: 'EMEA', country: 'Germany', status: 'paid', amount: 330 },
|
|
48
|
+
{ id: 'O-6', region: 'EMEA', country: 'Germany', status: 'returned', amount: 205 },
|
|
49
|
+
{ id: 'O-7', region: 'EMEA', country: 'France', status: 'open', amount: 155 },
|
|
50
|
+
{ id: 'O-8', region: 'EMEA', country: 'France', status: 'shipped', amount: 420 },
|
|
51
|
+
{ id: 'O-9', region: 'APAC', country: 'Japan', status: 'paid', amount: 890 },
|
|
52
|
+
{ id: 'O-10', region: 'APAC', country: 'Japan', status: 'returned', amount: 240 },
|
|
53
|
+
{ id: 'O-11', region: 'APAC', country: 'Australia', status: 'open', amount: 175 },
|
|
54
|
+
{ id: 'O-12', region: 'APAC', country: 'Singapore', status: 'delivered', amount: 1200 },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const cols: ColumnDef<typeof features, Order>[] = [
|
|
58
|
+
{ field: 'id', header: 'ID', width: 90 },
|
|
59
|
+
{ field: 'region', header: 'Region', width: 110 },
|
|
60
|
+
{ field: 'country', header: 'Country', width: 160 },
|
|
61
|
+
{ field: 'status', header: 'Status', width: 110 },
|
|
62
|
+
{ field: 'amount', header: 'Amount', width: 100, align: 'right' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
function mountGrid() {
|
|
66
|
+
return new Promise<{
|
|
67
|
+
api: SvGridApi<typeof features, Order>
|
|
68
|
+
destroy: () => void
|
|
69
|
+
}>((res, rej) => {
|
|
70
|
+
const target = document.createElement('div')
|
|
71
|
+
document.body.appendChild(target)
|
|
72
|
+
let api: SvGridApi<typeof features, Order> | null = null
|
|
73
|
+
const app = mount(SvGrid, {
|
|
74
|
+
target,
|
|
75
|
+
props: {
|
|
76
|
+
data: rows,
|
|
77
|
+
columns: cols,
|
|
78
|
+
features,
|
|
79
|
+
_rowModels: {
|
|
80
|
+
coreRowModel: createCoreRowModel(),
|
|
81
|
+
filteredRowModel: createFilteredRowModel(),
|
|
82
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
83
|
+
},
|
|
84
|
+
containerHeight: 360,
|
|
85
|
+
virtualization: false,
|
|
86
|
+
columnVirtualization: false,
|
|
87
|
+
showPagination: false,
|
|
88
|
+
showColumnFilters: false,
|
|
89
|
+
showGlobalFilter: false,
|
|
90
|
+
showRowSelection: false,
|
|
91
|
+
onApiReady(received: SvGridApi<typeof features, Order>) {
|
|
92
|
+
api = received
|
|
93
|
+
res({ api, destroy: () => { unmount(app); target.remove() } })
|
|
94
|
+
},
|
|
95
|
+
} as any,
|
|
96
|
+
})
|
|
97
|
+
queueMicrotask(() => { if (!api) rej(new Error('onApiReady never fired')) })
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
102
|
+
|
|
103
|
+
describe('SvGridApi.setFacetFilter - single column', () => {
|
|
104
|
+
it('narrows to one allowed value', async () => {
|
|
105
|
+
const { api, destroy } = await mountGrid()
|
|
106
|
+
try {
|
|
107
|
+
api.setFacetFilter('region', ['EMEA'])
|
|
108
|
+
await tick()
|
|
109
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
110
|
+
expect(ids).toEqual(['O-5', 'O-6', 'O-7', 'O-8'])
|
|
111
|
+
} finally { destroy() }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('accepts multiple values (OR within column)', async () => {
|
|
115
|
+
const { api, destroy } = await mountGrid()
|
|
116
|
+
try {
|
|
117
|
+
api.setFacetFilter('region', ['EMEA', 'APAC'])
|
|
118
|
+
await tick()
|
|
119
|
+
const regions = new Set(api.getDisplayedRows().map((r) => r.region))
|
|
120
|
+
expect(regions).toEqual(new Set(['EMEA', 'APAC']))
|
|
121
|
+
expect(api.getDisplayedRows().length).toBe(8)
|
|
122
|
+
} finally { destroy() }
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('empty allowed array filters everything out', async () => {
|
|
126
|
+
// An empty SET is treated as a clear (matches the implementation -
|
|
127
|
+
// see `setFacetFilter`'s null branch). Verify the documented
|
|
128
|
+
// behavior: empty array clears.
|
|
129
|
+
const { api, destroy } = await mountGrid()
|
|
130
|
+
try {
|
|
131
|
+
api.setFacetFilter('region', [])
|
|
132
|
+
await tick()
|
|
133
|
+
expect(api.getDisplayedRows().length).toBe(rows.length)
|
|
134
|
+
} finally { destroy() }
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('null clears the filter', async () => {
|
|
138
|
+
const { api, destroy } = await mountGrid()
|
|
139
|
+
try {
|
|
140
|
+
api.setFacetFilter('region', ['EMEA'])
|
|
141
|
+
await tick()
|
|
142
|
+
expect(api.getDisplayedRows().length).toBe(4)
|
|
143
|
+
api.setFacetFilter('region', null)
|
|
144
|
+
await tick()
|
|
145
|
+
expect(api.getDisplayedRows().length).toBe(rows.length)
|
|
146
|
+
} finally { destroy() }
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('SvGridApi.setFacetFilter - multi-column AND', () => {
|
|
151
|
+
it('region + status both narrow', async () => {
|
|
152
|
+
const { api, destroy } = await mountGrid()
|
|
153
|
+
try {
|
|
154
|
+
api.setFacetFilter('region', ['EMEA'])
|
|
155
|
+
api.setFacetFilter('status', ['paid', 'returned'])
|
|
156
|
+
await tick()
|
|
157
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
158
|
+
expect(ids).toEqual(['O-5', 'O-6']) // EMEA rows whose status is paid or returned
|
|
159
|
+
} finally { destroy() }
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('region + country + status all compose', async () => {
|
|
163
|
+
const { api, destroy } = await mountGrid()
|
|
164
|
+
try {
|
|
165
|
+
api.setFacetFilter('region', ['APAC'])
|
|
166
|
+
api.setFacetFilter('country', ['Japan', 'Singapore'])
|
|
167
|
+
api.setFacetFilter('status', ['delivered'])
|
|
168
|
+
await tick()
|
|
169
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
170
|
+
expect(ids).toEqual(['O-12']) // APAC + Singapore + delivered
|
|
171
|
+
} finally { destroy() }
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('SvGridApi.setFacetFilter - composition with operator filters', () => {
|
|
176
|
+
it('facet narrows; setFilter further narrows on the same column id', async () => {
|
|
177
|
+
const { api, destroy } = await mountGrid()
|
|
178
|
+
try {
|
|
179
|
+
api.setFacetFilter('region', ['Americas', 'EMEA'])
|
|
180
|
+
api.setFilter('country', { operator: 'startsWith', value: 'g' }) // → Germany
|
|
181
|
+
await tick()
|
|
182
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
183
|
+
expect(ids).toEqual(['O-5', 'O-6'])
|
|
184
|
+
} finally { destroy() }
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('clearing the facet but keeping setFilter only honors setFilter', async () => {
|
|
188
|
+
const { api, destroy } = await mountGrid()
|
|
189
|
+
try {
|
|
190
|
+
api.setFacetFilter('region', ['EMEA'])
|
|
191
|
+
api.setFilter('country', { operator: 'contains', value: 'fran' })
|
|
192
|
+
await tick()
|
|
193
|
+
expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8'])
|
|
194
|
+
|
|
195
|
+
api.setFacetFilter('region', null)
|
|
196
|
+
await tick()
|
|
197
|
+
expect(api.getDisplayedRows().map((r) => r.id)).toEqual(['O-7', 'O-8'])
|
|
198
|
+
} finally { destroy() }
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('SvGridApi.setFacetFilter - edge cases', () => {
|
|
203
|
+
it('unknown allowed values silently match nothing', async () => {
|
|
204
|
+
const { api, destroy } = await mountGrid()
|
|
205
|
+
try {
|
|
206
|
+
api.setFacetFilter('status', ['SHIPPED_IN_TRANSIT', 'PENDING_REVIEW'])
|
|
207
|
+
await tick()
|
|
208
|
+
expect(api.getDisplayedRows().length).toBe(0)
|
|
209
|
+
} finally { destroy() }
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('mixed known + unknown values: known values still match', async () => {
|
|
213
|
+
const { api, destroy } = await mountGrid()
|
|
214
|
+
try {
|
|
215
|
+
api.setFacetFilter('status', ['shipped', 'UNKNOWN_VALUE'])
|
|
216
|
+
await tick()
|
|
217
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
218
|
+
expect(ids).toEqual(['O-4', 'O-8'])
|
|
219
|
+
} finally { destroy() }
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('SvGridApi.setFacetFilter - simulates the tree-list pattern', () => {
|
|
224
|
+
// Demo 111's tree picker resolves a hierarchical selection to a flat
|
|
225
|
+
// list of leaf values, then calls setFacetFilter. These tests
|
|
226
|
+
// exercise that contract.
|
|
227
|
+
|
|
228
|
+
it('simulates a tree drop: select Americas → all American countries', async () => {
|
|
229
|
+
const { api, destroy } = await mountGrid()
|
|
230
|
+
try {
|
|
231
|
+
// Leaf descendants of the "Americas" tree node:
|
|
232
|
+
const americansCountries = ['United States', 'Canada', 'Brazil']
|
|
233
|
+
api.setFacetFilter('country', americansCountries)
|
|
234
|
+
await tick()
|
|
235
|
+
const regions = new Set(api.getDisplayedRows().map((r) => r.region))
|
|
236
|
+
expect(regions).toEqual(new Set(['Americas']))
|
|
237
|
+
} finally { destroy() }
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('simulates a partial-tree selection: only Japan + Singapore', async () => {
|
|
241
|
+
const { api, destroy } = await mountGrid()
|
|
242
|
+
try {
|
|
243
|
+
api.setFacetFilter('country', ['Japan', 'Singapore'])
|
|
244
|
+
await tick()
|
|
245
|
+
const ids = api.getDisplayedRows().map((r) => r.id)
|
|
246
|
+
expect(ids).toEqual(['O-9', 'O-10', 'O-12'])
|
|
247
|
+
} finally { destroy() }
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('SvGridApi.setFacetFilter - simulates the async loader pattern', () => {
|
|
252
|
+
// Demo 111's async card emulates a server returning the distinct
|
|
253
|
+
// emails. The grid is told the final selection via setFacetFilter -
|
|
254
|
+
// there's no special "loading" code path inside the grid, the
|
|
255
|
+
// consumer's UI handles that. Verify the contract works regardless of
|
|
256
|
+
// whether the values were enumerated client-side or server-side.
|
|
257
|
+
|
|
258
|
+
it('async-loaded values: select user-resolved subset', async () => {
|
|
259
|
+
const { api, destroy } = await mountGrid()
|
|
260
|
+
try {
|
|
261
|
+
// Pretend the user clicked these from a server-loaded value list.
|
|
262
|
+
const userSelected = ['Germany', 'France']
|
|
263
|
+
api.setFacetFilter('country', userSelected)
|
|
264
|
+
await tick()
|
|
265
|
+
const regions = new Set(api.getDisplayedRows().map((r) => r.region))
|
|
266
|
+
expect(regions).toEqual(new Set(['EMEA']))
|
|
267
|
+
expect(api.getDisplayedRows().length).toBe(4)
|
|
268
|
+
} finally { destroy() }
|
|
269
|
+
})
|
|
270
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import SvGrid from './SvGrid.svelte'
|
|
5
|
+
|
|
6
|
+
describe('SvGrid wrapper', () => {
|
|
7
|
+
it('exports a production wrapper component', () => {
|
|
8
|
+
expect(SvGrid).toBeDefined()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('contains full-wrapper controls and aria-activedescendant wiring', () => {
|
|
12
|
+
const source = readFileSync(resolve(__dirname, './SvGrid.svelte'), 'utf-8')
|
|
13
|
+
expect(source).toContain('Filter all rows')
|
|
14
|
+
expect(source).toContain('Previous')
|
|
15
|
+
expect(source).toContain('role="status"')
|
|
16
|
+
expect(source).toContain('role="alert"')
|
|
17
|
+
expect(source).toContain('activeDescendantId')
|
|
18
|
+
expect(source).toContain('getGridRootA11yProps')
|
|
19
|
+
expect(source).toContain('data-svgrid-header-col')
|
|
20
|
+
expect(source).toContain('data-svgrid-row')
|
|
21
|
+
expect(source).toContain('createSvelteVirtualizer')
|
|
22
|
+
expect(source).toContain('props.virtualization ?? true')
|
|
23
|
+
expect(source).toContain('virtualizer.getVirtualItems()')
|
|
24
|
+
expect(source).toContain('createColumnVirtualizer')
|
|
25
|
+
expect(source).toContain('data-selected-range')
|
|
26
|
+
expect(source).toContain('enableInlineEditing')
|
|
27
|
+
expect(source).toContain('showFilterMenu')
|
|
28
|
+
expect(source).toContain('<tfoot')
|
|
29
|
+
expect(source).toContain('editorType === "number"')
|
|
30
|
+
expect(source).toContain('editorType === "date"')
|
|
31
|
+
expect(source).toContain('editorType === "checkbox"')
|
|
32
|
+
expect(source).toContain('applyExcelFilter')
|
|
33
|
+
expect(source).toContain('showFilterRow')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Vitest setup. jsdom doesn't ship ResizeObserver, IntersectionObserver,
|
|
2
|
+
// or the layout APIs SvGrid touches inside its mount effects. Provide
|
|
3
|
+
// minimal no-op stubs so component mounting completes without crashing.
|
|
4
|
+
|
|
5
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
6
|
+
// @ts-expect-error - assigning to a globalThis property the env doesn't ship
|
|
7
|
+
globalThis.ResizeObserver = class {
|
|
8
|
+
observe() {}
|
|
9
|
+
unobserve() {}
|
|
10
|
+
disconnect() {}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof globalThis.IntersectionObserver === 'undefined') {
|
|
15
|
+
// @ts-expect-error - same
|
|
16
|
+
globalThis.IntersectionObserver = class {
|
|
17
|
+
observe() {}
|
|
18
|
+
unobserve() {}
|
|
19
|
+
disconnect() {}
|
|
20
|
+
takeRecords() {
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// jsdom's HTMLElement.scrollIntoView is a no-op; some grid code calls it
|
|
27
|
+
// during the first effect. Make sure the method exists on every element.
|
|
28
|
+
if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
|
|
29
|
+
// @ts-expect-error - patching a missing DOM method
|
|
30
|
+
Element.prototype.scrollIntoView = function () {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// jsdom returns 0 for offset* and getBoundingClientRect; the grid only uses
|
|
34
|
+
// these for visual layout (column widths, virtualization windowing), so the
|
|
35
|
+
// zeros are harmless for behavioral tests.
|
|
36
|
+
|
|
37
|
+
export {}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount, unmount } from 'svelte'
|
|
3
|
+
import SvGrid from './SvGrid.svelte'
|
|
4
|
+
import {
|
|
5
|
+
createCoreRowModel,
|
|
6
|
+
createFilteredRowModel,
|
|
7
|
+
createSortedRowModel,
|
|
8
|
+
sortFns,
|
|
9
|
+
tableFeatures,
|
|
10
|
+
} from './index'
|
|
11
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
12
|
+
|
|
13
|
+
type Order = { id: string; symbol: string; price: number }
|
|
14
|
+
|
|
15
|
+
const features = tableFeatures({})
|
|
16
|
+
const seed: Order[] = [
|
|
17
|
+
{ id: 'a', symbol: 'AAPL', price: 100 },
|
|
18
|
+
{ id: 'b', symbol: 'MSFT', price: 200 },
|
|
19
|
+
{ id: 'c', symbol: 'NVDA', price: 300 },
|
|
20
|
+
]
|
|
21
|
+
const columns: ColumnDef<typeof features, Order>[] = [
|
|
22
|
+
{ field: 'symbol', header: 'Symbol' },
|
|
23
|
+
{ field: 'price', header: 'Price' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function mountGrid(): Promise<{ api: SvGridApi<typeof features, Order>; destroy: () => void }> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const target = document.createElement('div')
|
|
29
|
+
document.body.appendChild(target)
|
|
30
|
+
let captured: SvGridApi<typeof features, Order> | null = null
|
|
31
|
+
const app = mount(SvGrid, {
|
|
32
|
+
target,
|
|
33
|
+
props: {
|
|
34
|
+
data: seed,
|
|
35
|
+
columns,
|
|
36
|
+
features,
|
|
37
|
+
getRowId: (o: Order) => o.id,
|
|
38
|
+
_rowModels: {
|
|
39
|
+
coreRowModel: createCoreRowModel(),
|
|
40
|
+
filteredRowModel: createFilteredRowModel(),
|
|
41
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
42
|
+
},
|
|
43
|
+
containerHeight: 400,
|
|
44
|
+
virtualization: false,
|
|
45
|
+
onApiReady(api: SvGridApi<typeof features, Order>) {
|
|
46
|
+
captured = api
|
|
47
|
+
resolve({ api, destroy: () => { unmount(app); target.remove() } })
|
|
48
|
+
},
|
|
49
|
+
} as any,
|
|
50
|
+
})
|
|
51
|
+
queueMicrotask(() => { if (!captured) reject(new Error('onApiReady never fired')) })
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tick = () => Promise.resolve()
|
|
56
|
+
|
|
57
|
+
describe('api.applyTransaction', () => {
|
|
58
|
+
it('adds, updates (by id), and removes (by id) in one batch', async () => {
|
|
59
|
+
const { api, destroy } = await mountGrid()
|
|
60
|
+
try {
|
|
61
|
+
const result = api.applyTransaction({
|
|
62
|
+
add: [{ id: 'd', symbol: 'AMZN', price: 400 }],
|
|
63
|
+
update: [{ id: 'b', symbol: 'MSFT', price: 222 }],
|
|
64
|
+
remove: ['a'],
|
|
65
|
+
})
|
|
66
|
+
await tick()
|
|
67
|
+
expect(result).toEqual({ added: 1, updated: 1, removed: 1 })
|
|
68
|
+
const data = api.getData()
|
|
69
|
+
expect(data.map((o) => o.id)).toEqual(['b', 'c', 'd'])
|
|
70
|
+
expect(data.find((o) => o.id === 'b')?.price).toBe(222)
|
|
71
|
+
} finally {
|
|
72
|
+
destroy()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('removes by row reference too', async () => {
|
|
77
|
+
const { api, destroy } = await mountGrid()
|
|
78
|
+
try {
|
|
79
|
+
const ref = api.getData()[2]! // 'c'
|
|
80
|
+
const result = api.applyTransaction({ remove: [ref] })
|
|
81
|
+
await tick()
|
|
82
|
+
expect(result.removed).toBe(1)
|
|
83
|
+
expect(api.getData().map((o) => o.id)).toEqual(['a', 'b'])
|
|
84
|
+
} finally {
|
|
85
|
+
destroy()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('ignores unknown ids without throwing', async () => {
|
|
90
|
+
const { api, destroy } = await mountGrid()
|
|
91
|
+
try {
|
|
92
|
+
const result = api.applyTransaction({ update: [{ id: 'zzz', symbol: 'X', price: 1 }], remove: ['nope'] })
|
|
93
|
+
await tick()
|
|
94
|
+
expect(result).toEqual({ added: 0, updated: 0, removed: 0 })
|
|
95
|
+
expect(api.getData()).toHaveLength(3)
|
|
96
|
+
} finally {
|
|
97
|
+
destroy()
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createColumnVirtualizer } from './column-virtualizer'
|
|
3
|
+
|
|
4
|
+
describe('column virtualizer', () => {
|
|
5
|
+
it('computes horizontal window', () => {
|
|
6
|
+
const v = createColumnVirtualizer({
|
|
7
|
+
count: 1000,
|
|
8
|
+
viewportWidth: 700,
|
|
9
|
+
overscan: 3,
|
|
10
|
+
estimateSize: () => 140,
|
|
11
|
+
})
|
|
12
|
+
const items = v.getVirtualItems()
|
|
13
|
+
expect(items[0]?.index).toBe(0)
|
|
14
|
+
expect(items.length).toBeGreaterThan(0)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('updates indices on horizontal scroll', () => {
|
|
18
|
+
const v = createColumnVirtualizer({
|
|
19
|
+
count: 1000,
|
|
20
|
+
viewportWidth: 700,
|
|
21
|
+
estimateSize: () => 140,
|
|
22
|
+
})
|
|
23
|
+
v.setHorizontalOffset(2800)
|
|
24
|
+
const state = v.getState()
|
|
25
|
+
expect(state.startIndex).toBeGreaterThan(0)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
|
|
3
|
+
export type ColumnSizeEstimator = (index: number) => number
|
|
4
|
+
|
|
5
|
+
export function createColumnVirtualizer(input: {
|
|
6
|
+
count: number
|
|
7
|
+
viewportWidth: number
|
|
8
|
+
scrollOffset?: number
|
|
9
|
+
overscan?: number
|
|
10
|
+
/** Either a uniform size (number) or a per-column size function. */
|
|
11
|
+
estimateSize?: number | ColumnSizeEstimator
|
|
12
|
+
}) {
|
|
13
|
+
const virtualizer = createVirtualizer({
|
|
14
|
+
count: input.count,
|
|
15
|
+
estimateSize: input.estimateSize ?? 140,
|
|
16
|
+
viewportHeight: input.viewportWidth,
|
|
17
|
+
scrollOffset: input.scrollOffset ?? 0,
|
|
18
|
+
overscan: input.overscan ?? 4,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...virtualizer,
|
|
23
|
+
setViewportWidth(viewportWidth: number) {
|
|
24
|
+
virtualizer.setViewportHeight(viewportWidth)
|
|
25
|
+
},
|
|
26
|
+
setHorizontalOffset(scrollOffset: number) {
|
|
27
|
+
virtualizer.setScrollOffset(scrollOffset)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
import type { VirtualizerOptions } from './types'
|
|
3
|
+
|
|
4
|
+
export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
5
|
+
const virtualizer = createVirtualizer(options)
|
|
6
|
+
let version = $state(0)
|
|
7
|
+
|
|
8
|
+
virtualizer.subscribe(() => {
|
|
9
|
+
version += 1
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
get version() {
|
|
14
|
+
return version
|
|
15
|
+
},
|
|
16
|
+
setOptions: virtualizer.setOptions,
|
|
17
|
+
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
|
+
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
|
+
scrollToIndex: virtualizer.scrollToIndex,
|
|
20
|
+
getVirtualItems: virtualizer.getVirtualItems,
|
|
21
|
+
getTotalSize: virtualizer.getTotalSize,
|
|
22
|
+
getState: virtualizer.getState,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type VirtualItem = {
|
|
2
|
+
index: number
|
|
3
|
+
start: number
|
|
4
|
+
size: number
|
|
5
|
+
end: number
|
|
6
|
+
key: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type VirtualizerOptions = {
|
|
10
|
+
count: number
|
|
11
|
+
/**
|
|
12
|
+
* Per-item size. Pass a number for a uniform layout (fast path) or a
|
|
13
|
+
* function for variable sizing - the virtualizer will build cumulative
|
|
14
|
+
* offsets and use binary lookup to find the visible window. The function
|
|
15
|
+
* receives the item index and must return its pixel size.
|
|
16
|
+
*/
|
|
17
|
+
estimateSize: number | ((index: number) => number)
|
|
18
|
+
overscan?: number
|
|
19
|
+
viewportHeight: number
|
|
20
|
+
scrollOffset?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type VirtualizerState = {
|
|
24
|
+
items: Array<VirtualItem>
|
|
25
|
+
totalSize: number
|
|
26
|
+
startIndex: number
|
|
27
|
+
endIndex: number
|
|
28
|
+
scrollOffset: number
|
|
29
|
+
viewportHeight: number
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createVirtualizer } from './virtualizer'
|
|
3
|
+
|
|
4
|
+
describe('virtualizer', () => {
|
|
5
|
+
it('computes bounded virtual range with overscan', () => {
|
|
6
|
+
const v = createVirtualizer({
|
|
7
|
+
count: 100_000,
|
|
8
|
+
estimateSize: 36,
|
|
9
|
+
overscan: 8,
|
|
10
|
+
viewportHeight: 360,
|
|
11
|
+
scrollOffset: 0,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const items = v.getVirtualItems()
|
|
15
|
+
expect(items.length).toBeGreaterThan(0)
|
|
16
|
+
expect(items[0]?.index).toBe(0)
|
|
17
|
+
expect(items[items.length - 1]?.index).toBe(18)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('updates range when scroll offset changes', () => {
|
|
21
|
+
const v = createVirtualizer({
|
|
22
|
+
count: 100_000,
|
|
23
|
+
estimateSize: 40,
|
|
24
|
+
overscan: 5,
|
|
25
|
+
viewportHeight: 400,
|
|
26
|
+
scrollOffset: 0,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
v.setScrollOffset(4_000)
|
|
30
|
+
const state = v.getState()
|
|
31
|
+
expect(state.startIndex).toBeLessThanOrEqual(100)
|
|
32
|
+
expect(state.endIndex).toBeGreaterThanOrEqual(110)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('scrollToIndex clamps target offset', () => {
|
|
36
|
+
const v = createVirtualizer({
|
|
37
|
+
count: 100,
|
|
38
|
+
estimateSize: 30,
|
|
39
|
+
overscan: 2,
|
|
40
|
+
viewportHeight: 300,
|
|
41
|
+
scrollOffset: 0,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
v.scrollToIndex(9_999)
|
|
45
|
+
expect(v.getState().scrollOffset).toBe(2_700)
|
|
46
|
+
})
|
|
47
|
+
})
|