@svgrid/grid 2.6.3 → 2.6.5
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/dist/SvGrid.controller.svelte.d.ts +2 -1
- package/dist/SvGrid.controller.svelte.js +145 -3
- package/dist/SvGrid.css +32 -0
- package/dist/SvGrid.svelte +64 -6
- package/dist/SvGrid.types.d.ts +61 -0
- package/dist/a11y/grid-announcements.d.ts +58 -0
- package/dist/a11y/grid-announcements.js +70 -0
- package/dist/advanced-filter.svelte.d.ts +42 -0
- package/dist/advanced-filter.svelte.js +13 -0
- package/dist/build-api.js +44 -2
- package/dist/cdn/{GridMenus-BoVKgAv8.js → GridMenus-CBvjL5u4.js} +14 -14
- package/dist/cdn/{GridMenus-DL1zrDwf.js → GridMenus-DAzp6YXc.js} +14 -14
- package/dist/cdn/{src-M2NfKnjX.js → src-3WKN7L4G.js} +5932 -5743
- package/dist/cdn/{src-CEpw6Ato.js → src-Cl7h5-Ux.js} +3371 -3182
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/filter-operators.d.ts +2 -1
- package/dist/filter-operators.js +5 -6
- package/dist/filtering/excel-filters.d.ts +1 -0
- package/dist/filtering/excel-filters.js +5 -0
- package/dist/filtering/filter-operator-catalogue.d.ts +42 -0
- package/dist/filtering/filter-operator-catalogue.js +42 -0
- package/dist/filtering/predicate-expr.d.ts +78 -0
- package/dist/filtering/predicate-expr.js +1 -0
- package/dist/filtering/row-predicate.d.ts +17 -0
- package/dist/filtering/row-predicate.js +55 -0
- package/dist/grid-messages.d.ts +7 -0
- package/dist/grid-messages.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/server-data-source.d.ts +34 -0
- package/dist/server-data-source.js +19 -0
- package/dist/svgrid-wrapper.types.d.ts +37 -5
- package/dist/themes/index.js +12 -4
- package/package.json +2 -1
- package/src/SvGrid.controller.svelte.ts +169 -2
- package/src/SvGrid.css +32 -0
- package/src/SvGrid.svelte +64 -6
- package/src/SvGrid.types.ts +61 -0
- package/src/a11y/grid-announcements.test.ts +78 -0
- package/src/a11y/grid-announcements.ts +94 -0
- package/src/a11y.announce.test.ts +250 -0
- package/src/a11y.axe.test.ts +194 -0
- package/src/advanced-filter.svelte.ts +59 -0
- package/src/build-api.ts +43 -2
- package/src/filter-operators.ts +9 -6
- package/src/filtering/excel-filters.ts +15 -0
- package/src/filtering/filter-operator-catalogue.ts +74 -0
- package/src/filtering/node-entry.test.ts +75 -0
- package/src/filtering/predicate-expr.ts +50 -0
- package/src/filtering/row-predicate.ts +57 -0
- package/src/grid-messages.ts +18 -0
- package/src/index.ts +28 -0
- package/src/server-data-source.test.ts +94 -0
- package/src/server-data-source.ts +56 -0
- package/src/svgrid-wrapper.types.ts +37 -5
- package/src/svgrid.advanced-filter.test.ts +481 -0
- package/src/svgrid.displayed-rows.test.ts +144 -0
- package/src/svgrid.group-props-reactive.svelte.test.ts +151 -0
- package/src/svgrid.group-props.test.ts +280 -0
- package/src/themes/contrast.test.ts +112 -0
- package/src/themes/index.ts +12 -4
- package/themes/catppuccin.css +3 -3
- package/themes/dracula.css +3 -3
- package/themes/nord.css +3 -3
- package/themes/notion.css +4 -4
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for the advanced filter, exercised through a real mounted
|
|
3
|
+
* grid rather than the pipeline in isolation.
|
|
4
|
+
*
|
|
5
|
+
* The behaviour that matters here is not "does the predicate work" - that is
|
|
6
|
+
* settled by the compiler's differential tests in @svgrid/enterprise. It is:
|
|
7
|
+
*
|
|
8
|
+
* - the grid FAILS OPEN when no engine is registered, so the free package
|
|
9
|
+
* shows every row rather than none
|
|
10
|
+
* - a compile failure or a throw also fails open, never half-filters
|
|
11
|
+
* - it composes with AND against the global, column and facet filters
|
|
12
|
+
* - it runs LAST, so an engine sees only the rows those stages left
|
|
13
|
+
* - clearAllFilters() clears it, and view state round-trips it
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it, afterEach } from 'vitest'
|
|
16
|
+
import { mount, unmount } from 'svelte'
|
|
17
|
+
import SvGrid from './SvGrid.svelte'
|
|
18
|
+
import {
|
|
19
|
+
columnFilteringFeature,
|
|
20
|
+
createCoreRowModel,
|
|
21
|
+
createFilteredRowModel,
|
|
22
|
+
createSortedRowModel,
|
|
23
|
+
registerAdvancedFilterEngine,
|
|
24
|
+
rowSortingFeature,
|
|
25
|
+
sortFns,
|
|
26
|
+
tableFeatures,
|
|
27
|
+
} from './index'
|
|
28
|
+
import type {
|
|
29
|
+
AdvancedFilterEngine,
|
|
30
|
+
ColumnDef,
|
|
31
|
+
GridPredicateExpr,
|
|
32
|
+
SvGridApi,
|
|
33
|
+
} from './index'
|
|
34
|
+
|
|
35
|
+
type Order = { id: string; region: string; status: string; amount: number }
|
|
36
|
+
|
|
37
|
+
const features = tableFeatures({ columnFilteringFeature, rowSortingFeature })
|
|
38
|
+
|
|
39
|
+
const rows: Order[] = [
|
|
40
|
+
{ id: 'O-1', region: 'EMEA', status: 'open', amount: 100 },
|
|
41
|
+
{ id: 'O-2', region: 'EMEA', status: 'paid', amount: 200 },
|
|
42
|
+
{ id: 'O-3', region: 'APAC', status: 'open', amount: 300 },
|
|
43
|
+
{ id: 'O-4', region: 'APAC', status: 'paid', amount: 400 },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const cols: ColumnDef<typeof features, Order>[] = [
|
|
47
|
+
{ field: 'id', header: 'ID', width: 90 },
|
|
48
|
+
{ field: 'region', header: 'Region', width: 110 },
|
|
49
|
+
{ field: 'status', header: 'Status', width: 110 },
|
|
50
|
+
{ field: 'amount', header: 'Amount', width: 100 },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
function mountGrid(props: Record<string, unknown> = {}) {
|
|
54
|
+
return new Promise<{
|
|
55
|
+
api: SvGridApi<typeof features, Order>
|
|
56
|
+
target: HTMLElement
|
|
57
|
+
destroy: () => void
|
|
58
|
+
}>(
|
|
59
|
+
(res, rej) => {
|
|
60
|
+
const target = document.createElement('div')
|
|
61
|
+
document.body.appendChild(target)
|
|
62
|
+
let api: SvGridApi<typeof features, Order> | null = null
|
|
63
|
+
const app = mount(SvGrid, {
|
|
64
|
+
target,
|
|
65
|
+
props: {
|
|
66
|
+
data: rows,
|
|
67
|
+
columns: cols,
|
|
68
|
+
features,
|
|
69
|
+
_rowModels: {
|
|
70
|
+
coreRowModel: createCoreRowModel(),
|
|
71
|
+
filteredRowModel: createFilteredRowModel(),
|
|
72
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
73
|
+
},
|
|
74
|
+
containerHeight: 360,
|
|
75
|
+
virtualization: false,
|
|
76
|
+
columnVirtualization: false,
|
|
77
|
+
showPagination: false,
|
|
78
|
+
...props,
|
|
79
|
+
onApiReady(received: SvGridApi<typeof features, Order>) {
|
|
80
|
+
api = received
|
|
81
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
82
|
+
},
|
|
83
|
+
} as never,
|
|
84
|
+
})
|
|
85
|
+
queueMicrotask(() => { if (!api) rej(new Error('onApiReady never fired')) })
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
91
|
+
const ids = (api: SvGridApi<typeof features, Order>) =>
|
|
92
|
+
api.getDisplayedRows().map((r) => (r as unknown as Order).id)
|
|
93
|
+
|
|
94
|
+
/** An engine that honours only a flat `cmp` on `region` with `equals`. */
|
|
95
|
+
const regionEqualsEngine: AdvancedFilterEngine = (expr, ctx) => {
|
|
96
|
+
if (expr.kind !== 'cmp' || expr.op !== 'equals') return null
|
|
97
|
+
const want = String(expr.value ?? '')
|
|
98
|
+
return (row) => String(ctx.getValue(row, expr.column) ?? '') === want
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const EMEA: GridPredicateExpr = { kind: 'cmp', column: 'region', op: 'equals', value: 'EMEA' }
|
|
102
|
+
|
|
103
|
+
afterEach(() => registerAdvancedFilterEngine(null))
|
|
104
|
+
|
|
105
|
+
describe('advanced filter without an engine (free grid)', () => {
|
|
106
|
+
it('shows every row - fails OPEN rather than filtering nothing through', async () => {
|
|
107
|
+
const { api, destroy } = await mountGrid()
|
|
108
|
+
try {
|
|
109
|
+
api.setAdvancedFilter(EMEA)
|
|
110
|
+
await tick()
|
|
111
|
+
expect(ids(api)).toEqual(['O-1', 'O-2', 'O-3', 'O-4'])
|
|
112
|
+
} finally {
|
|
113
|
+
destroy()
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('still records the expression, and reports itself inactive', async () => {
|
|
118
|
+
const { api, destroy } = await mountGrid()
|
|
119
|
+
try {
|
|
120
|
+
api.setAdvancedFilter(EMEA)
|
|
121
|
+
await tick()
|
|
122
|
+
expect(api.getAdvancedFilter()).toEqual(EMEA)
|
|
123
|
+
expect(api.isAdvancedFilterActive()).toBe(false)
|
|
124
|
+
} finally {
|
|
125
|
+
destroy()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('advanced filter with an engine', () => {
|
|
131
|
+
it('narrows the rows', async () => {
|
|
132
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
133
|
+
const { api, destroy } = await mountGrid()
|
|
134
|
+
try {
|
|
135
|
+
api.setAdvancedFilter(EMEA)
|
|
136
|
+
await tick()
|
|
137
|
+
expect(ids(api)).toEqual(['O-1', 'O-2'])
|
|
138
|
+
expect(api.isAdvancedFilterActive()).toBe(true)
|
|
139
|
+
} finally {
|
|
140
|
+
destroy()
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('clearing restores every row', async () => {
|
|
145
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
146
|
+
const { api, destroy } = await mountGrid()
|
|
147
|
+
try {
|
|
148
|
+
api.setAdvancedFilter(EMEA)
|
|
149
|
+
await tick()
|
|
150
|
+
api.clearAdvancedFilter()
|
|
151
|
+
await tick()
|
|
152
|
+
expect(ids(api)).toHaveLength(4)
|
|
153
|
+
expect(api.getAdvancedFilter()).toBeNull()
|
|
154
|
+
} finally {
|
|
155
|
+
destroy()
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('fails open when the engine cannot compile the expression', async () => {
|
|
160
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
161
|
+
const { api, destroy } = await mountGrid()
|
|
162
|
+
try {
|
|
163
|
+
// `contains` is outside what this engine handles, so it returns null.
|
|
164
|
+
api.setAdvancedFilter({ kind: 'cmp', column: 'region', op: 'contains', value: 'EM' })
|
|
165
|
+
await tick()
|
|
166
|
+
expect(ids(api)).toHaveLength(4)
|
|
167
|
+
} finally {
|
|
168
|
+
destroy()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('fails open when the engine throws', async () => {
|
|
173
|
+
registerAdvancedFilterEngine(() => {
|
|
174
|
+
throw new Error('boom')
|
|
175
|
+
})
|
|
176
|
+
const { api, destroy } = await mountGrid()
|
|
177
|
+
try {
|
|
178
|
+
api.setAdvancedFilter(EMEA)
|
|
179
|
+
await tick()
|
|
180
|
+
expect(ids(api)).toHaveLength(4)
|
|
181
|
+
} finally {
|
|
182
|
+
destroy()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('fails open when the compiled predicate itself throws', async () => {
|
|
187
|
+
registerAdvancedFilterEngine(() => () => {
|
|
188
|
+
throw new Error('boom per row')
|
|
189
|
+
})
|
|
190
|
+
const { api, destroy } = await mountGrid()
|
|
191
|
+
try {
|
|
192
|
+
api.setAdvancedFilter(EMEA)
|
|
193
|
+
await tick()
|
|
194
|
+
expect(ids(api)).toHaveLength(4)
|
|
195
|
+
} finally {
|
|
196
|
+
destroy()
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('composition with the other filter stages', () => {
|
|
202
|
+
it('ANDs with a column filter', async () => {
|
|
203
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
204
|
+
const { api, destroy } = await mountGrid()
|
|
205
|
+
try {
|
|
206
|
+
api.setAdvancedFilter(EMEA)
|
|
207
|
+
api.setFilter('status', { operator: 'equals', value: 'paid' })
|
|
208
|
+
await tick()
|
|
209
|
+
expect(ids(api)).toEqual(['O-2'])
|
|
210
|
+
} finally {
|
|
211
|
+
destroy()
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('ANDs with a facet filter', async () => {
|
|
216
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
217
|
+
const { api, destroy } = await mountGrid()
|
|
218
|
+
try {
|
|
219
|
+
api.setAdvancedFilter(EMEA)
|
|
220
|
+
api.setFacetFilter('status', ['open'])
|
|
221
|
+
await tick()
|
|
222
|
+
expect(ids(api)).toEqual(['O-1'])
|
|
223
|
+
} finally {
|
|
224
|
+
destroy()
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('runs LAST - the engine only sees rows the earlier stages kept', async () => {
|
|
229
|
+
let seen: number | null = null
|
|
230
|
+
registerAdvancedFilterEngine((_expr, ctx) => {
|
|
231
|
+
seen = ctx.rows.length
|
|
232
|
+
return () => true
|
|
233
|
+
})
|
|
234
|
+
const { api, destroy } = await mountGrid()
|
|
235
|
+
try {
|
|
236
|
+
api.setFilter('region', { operator: 'equals', value: 'EMEA' })
|
|
237
|
+
api.setAdvancedFilter(EMEA)
|
|
238
|
+
await tick()
|
|
239
|
+
// 2 EMEA rows, not the full 4: aggregates fold over the narrowed set.
|
|
240
|
+
expect(seen).toBe(2)
|
|
241
|
+
} finally {
|
|
242
|
+
destroy()
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('is cleared by clearAllFilters(), which promises every filter surface', async () => {
|
|
247
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
248
|
+
const { api, destroy } = await mountGrid()
|
|
249
|
+
try {
|
|
250
|
+
api.setAdvancedFilter(EMEA)
|
|
251
|
+
api.setFilter('status', { operator: 'equals', value: 'paid' })
|
|
252
|
+
await tick()
|
|
253
|
+
api.clearAllFilters()
|
|
254
|
+
await tick()
|
|
255
|
+
expect(api.getAdvancedFilter()).toBeNull()
|
|
256
|
+
expect(ids(api)).toHaveLength(4)
|
|
257
|
+
} finally {
|
|
258
|
+
destroy()
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('seeding and view state', () => {
|
|
264
|
+
it('seeds from initialAdvancedFilter at mount', async () => {
|
|
265
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
266
|
+
const { api, destroy } = await mountGrid({ initialAdvancedFilter: EMEA })
|
|
267
|
+
try {
|
|
268
|
+
await tick()
|
|
269
|
+
expect(api.getAdvancedFilter()).toEqual(EMEA)
|
|
270
|
+
expect(ids(api)).toEqual(['O-1', 'O-2'])
|
|
271
|
+
} finally {
|
|
272
|
+
destroy()
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('omits the key from getState() when no advanced filter is set', async () => {
|
|
277
|
+
const { api, destroy } = await mountGrid()
|
|
278
|
+
try {
|
|
279
|
+
// Existing saved views must round-trip byte-identical, so the key is
|
|
280
|
+
// absent rather than present-and-null.
|
|
281
|
+
expect('advancedFilter' in api.getState()).toBe(false)
|
|
282
|
+
} finally {
|
|
283
|
+
destroy()
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('round-trips through getState / setState', async () => {
|
|
288
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
289
|
+
const { api, destroy } = await mountGrid()
|
|
290
|
+
try {
|
|
291
|
+
api.setAdvancedFilter(EMEA)
|
|
292
|
+
await tick()
|
|
293
|
+
const saved = JSON.parse(JSON.stringify(api.getState()))
|
|
294
|
+
api.clearAdvancedFilter()
|
|
295
|
+
await tick()
|
|
296
|
+
expect(ids(api)).toHaveLength(4)
|
|
297
|
+
|
|
298
|
+
api.setState(saved)
|
|
299
|
+
await tick()
|
|
300
|
+
expect(api.getAdvancedFilter()).toEqual(EMEA)
|
|
301
|
+
expect(ids(api)).toEqual(['O-1', 'O-2'])
|
|
302
|
+
} finally {
|
|
303
|
+
destroy()
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('leaves the filter alone when setState omits the key', async () => {
|
|
308
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
309
|
+
const { api, destroy } = await mountGrid()
|
|
310
|
+
try {
|
|
311
|
+
api.setAdvancedFilter(EMEA)
|
|
312
|
+
await tick()
|
|
313
|
+
api.setState({ globalFilter: '' })
|
|
314
|
+
await tick()
|
|
315
|
+
expect(api.getAdvancedFilter()).toEqual(EMEA)
|
|
316
|
+
} finally {
|
|
317
|
+
destroy()
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('clears when setState passes an explicit null', async () => {
|
|
322
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
323
|
+
const { api, destroy } = await mountGrid()
|
|
324
|
+
try {
|
|
325
|
+
api.setAdvancedFilter(EMEA)
|
|
326
|
+
await tick()
|
|
327
|
+
api.setState({ advancedFilter: null })
|
|
328
|
+
await tick()
|
|
329
|
+
expect(api.getAdvancedFilter()).toBeNull()
|
|
330
|
+
expect(ids(api)).toHaveLength(4)
|
|
331
|
+
} finally {
|
|
332
|
+
destroy()
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('toolbar indicator', () => {
|
|
338
|
+
// A filter set from a consumer-mounted panel hides rows with nothing in the
|
|
339
|
+
// grid to explain it. The chip is the grid's own account of that state.
|
|
340
|
+
const chip = (t: HTMLElement) => t.querySelector('.sv-grid-advf-chip')
|
|
341
|
+
|
|
342
|
+
it('is absent until an advanced filter is set', async () => {
|
|
343
|
+
const { target, destroy } = await mountGrid()
|
|
344
|
+
try {
|
|
345
|
+
await tick()
|
|
346
|
+
expect(chip(target)).toBeNull()
|
|
347
|
+
} finally {
|
|
348
|
+
destroy()
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('appears when a filter is set, even with no engine registered', async () => {
|
|
353
|
+
// Deliberate: the expression IS set, so the state is real and worth
|
|
354
|
+
// showing. Whether an engine acts on it is a separate concern, and the
|
|
355
|
+
// panel reports that separately.
|
|
356
|
+
const { api, target, destroy } = await mountGrid()
|
|
357
|
+
try {
|
|
358
|
+
api.setAdvancedFilter(EMEA)
|
|
359
|
+
await tick()
|
|
360
|
+
expect(chip(target)).not.toBeNull()
|
|
361
|
+
} finally {
|
|
362
|
+
destroy()
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('clears the filter and restores the rows', async () => {
|
|
367
|
+
registerAdvancedFilterEngine(regionEqualsEngine)
|
|
368
|
+
const { api, target, destroy } = await mountGrid()
|
|
369
|
+
try {
|
|
370
|
+
api.setAdvancedFilter(EMEA)
|
|
371
|
+
await tick()
|
|
372
|
+
expect(ids(api)).toEqual(['O-1', 'O-2'])
|
|
373
|
+
|
|
374
|
+
;(chip(target)!.querySelector('.sv-grid-advf-clear') as HTMLButtonElement).click()
|
|
375
|
+
await tick()
|
|
376
|
+
|
|
377
|
+
expect(api.getAdvancedFilter()).toBeNull()
|
|
378
|
+
expect(ids(api)).toEqual(['O-1', 'O-2', 'O-3', 'O-4'])
|
|
379
|
+
expect(chip(target)).toBeNull()
|
|
380
|
+
} finally {
|
|
381
|
+
destroy()
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('labels the clear control for assistive tech and translates it', async () => {
|
|
386
|
+
const { api, target, destroy } = await mountGrid({
|
|
387
|
+
localization: { text: { advancedFilterActive: 'Geavanceerd filter' } },
|
|
388
|
+
})
|
|
389
|
+
try {
|
|
390
|
+
api.setAdvancedFilter(EMEA)
|
|
391
|
+
await tick()
|
|
392
|
+
expect(chip(target)!.textContent).toContain('Geavanceerd filter')
|
|
393
|
+
expect(
|
|
394
|
+
chip(target)!.querySelector('.sv-grid-advf-clear')!.getAttribute('aria-label'),
|
|
395
|
+
).toBe('Clear advanced filter')
|
|
396
|
+
} finally {
|
|
397
|
+
destroy()
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
describe('onAdvancedFilterChange', () => {
|
|
403
|
+
// The authoring panel lives outside the grid, so a change the grid makes on
|
|
404
|
+
// its own has to be announced or the two drift apart.
|
|
405
|
+
it('does not fire on mount', async () => {
|
|
406
|
+
const seen: Array<GridPredicateExpr | null> = []
|
|
407
|
+
const { destroy } = await mountGrid({
|
|
408
|
+
onAdvancedFilterChange: (e: GridPredicateExpr | null) => seen.push(e),
|
|
409
|
+
})
|
|
410
|
+
try {
|
|
411
|
+
await tick()
|
|
412
|
+
expect(seen).toEqual([])
|
|
413
|
+
} finally {
|
|
414
|
+
destroy()
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('fires when the filter is set and when it is cleared', async () => {
|
|
419
|
+
const seen: Array<GridPredicateExpr | null> = []
|
|
420
|
+
const { api, destroy } = await mountGrid({
|
|
421
|
+
onAdvancedFilterChange: (e: GridPredicateExpr | null) => seen.push(e),
|
|
422
|
+
})
|
|
423
|
+
try {
|
|
424
|
+
api.setAdvancedFilter(EMEA)
|
|
425
|
+
await tick()
|
|
426
|
+
api.clearAdvancedFilter()
|
|
427
|
+
await tick()
|
|
428
|
+
expect(seen).toEqual([EMEA, null])
|
|
429
|
+
} finally {
|
|
430
|
+
destroy()
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('fires when the toolbar chip clears it', async () => {
|
|
435
|
+
const seen: Array<GridPredicateExpr | null> = []
|
|
436
|
+
const { api, target, destroy } = await mountGrid({
|
|
437
|
+
onAdvancedFilterChange: (e: GridPredicateExpr | null) => seen.push(e),
|
|
438
|
+
})
|
|
439
|
+
try {
|
|
440
|
+
api.setAdvancedFilter(EMEA)
|
|
441
|
+
await tick()
|
|
442
|
+
;(target.querySelector('.sv-grid-advf-clear') as HTMLButtonElement).click()
|
|
443
|
+
await tick()
|
|
444
|
+
expect(seen.at(-1)).toBeNull()
|
|
445
|
+
} finally {
|
|
446
|
+
destroy()
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('fires when clearAllFilters sweeps it away', async () => {
|
|
451
|
+
const seen: Array<GridPredicateExpr | null> = []
|
|
452
|
+
const { api, destroy } = await mountGrid({
|
|
453
|
+
onAdvancedFilterChange: (e: GridPredicateExpr | null) => seen.push(e),
|
|
454
|
+
})
|
|
455
|
+
try {
|
|
456
|
+
api.setAdvancedFilter(EMEA)
|
|
457
|
+
await tick()
|
|
458
|
+
api.clearAllFilters()
|
|
459
|
+
await tick()
|
|
460
|
+
expect(seen.at(-1)).toBeNull()
|
|
461
|
+
} finally {
|
|
462
|
+
destroy()
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('does not fire again when the same expression is re-applied', async () => {
|
|
467
|
+
const seen: Array<GridPredicateExpr | null> = []
|
|
468
|
+
const { api, destroy } = await mountGrid({
|
|
469
|
+
onAdvancedFilterChange: (e: GridPredicateExpr | null) => seen.push(e),
|
|
470
|
+
})
|
|
471
|
+
try {
|
|
472
|
+
api.setAdvancedFilter(EMEA)
|
|
473
|
+
await tick()
|
|
474
|
+
api.setAdvancedFilter({ ...EMEA })
|
|
475
|
+
await tick()
|
|
476
|
+
expect(seen).toHaveLength(1)
|
|
477
|
+
} finally {
|
|
478
|
+
destroy()
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api.getDisplayedRows()` returns the TData rows the body is rendering.
|
|
3
|
+
*
|
|
4
|
+
* The subtle case is what counts as "not a data row". Grouping banners are
|
|
5
|
+
* synthetic - they wrap an aggregate and must be skipped. Tree parents also
|
|
6
|
+
* have `subRows`, but they are real data rows that render their own cells, so
|
|
7
|
+
* they must be INCLUDED. A `subRows.length` test cannot tell the two apart and
|
|
8
|
+
* silently dropped every tree parent (and, through it, every tree parent in an
|
|
9
|
+
* enterprise export, whose default row source is this method).
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
12
|
+
import { mount, unmount } from 'svelte'
|
|
13
|
+
import SvGrid from './SvGrid.svelte'
|
|
14
|
+
import {
|
|
15
|
+
columnGroupingFeature,
|
|
16
|
+
createCoreRowModel,
|
|
17
|
+
createExpandedRowModel,
|
|
18
|
+
createFilteredRowModel,
|
|
19
|
+
createGroupedRowModel,
|
|
20
|
+
createPaginatedRowModel,
|
|
21
|
+
createSortedRowModel,
|
|
22
|
+
rowExpandingFeature,
|
|
23
|
+
rowSortingFeature,
|
|
24
|
+
sortFns,
|
|
25
|
+
tableFeatures,
|
|
26
|
+
} from './index'
|
|
27
|
+
import type { SvGridApi } from './index'
|
|
28
|
+
|
|
29
|
+
const features = tableFeatures({ rowSortingFeature, columnGroupingFeature, rowExpandingFeature })
|
|
30
|
+
const tick = async (n = 3) => {
|
|
31
|
+
for (let i = 0; i < n; i++) await new Promise((r) => setTimeout(r, 0))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const mounted: Array<() => void> = []
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
while (mounted.length) mounted.pop()!()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
function mountGrid(rows: unknown[], cols: unknown, extra: Record<string, unknown> = {}) {
|
|
40
|
+
const target = document.createElement('div')
|
|
41
|
+
document.body.appendChild(target)
|
|
42
|
+
let api: SvGridApi<never, never> | null = null
|
|
43
|
+
const app = mount(SvGrid, {
|
|
44
|
+
target,
|
|
45
|
+
props: {
|
|
46
|
+
data: rows,
|
|
47
|
+
columns: cols,
|
|
48
|
+
features,
|
|
49
|
+
_rowModels: {
|
|
50
|
+
coreRowModel: createCoreRowModel(),
|
|
51
|
+
filteredRowModel: createFilteredRowModel(),
|
|
52
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
53
|
+
groupedRowModel: createGroupedRowModel(),
|
|
54
|
+
expandedRowModel: createExpandedRowModel(),
|
|
55
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
56
|
+
},
|
|
57
|
+
containerHeight: 600,
|
|
58
|
+
virtualization: false,
|
|
59
|
+
onApiReady: (next: SvGridApi<never, never>) => (api = next),
|
|
60
|
+
...extra,
|
|
61
|
+
} as never,
|
|
62
|
+
})
|
|
63
|
+
mounted.push(() => unmount(app))
|
|
64
|
+
return { target, get api() { return api! } }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const flat = [
|
|
68
|
+
{ id: 1, name: 'Ada', department: 'Engineering' },
|
|
69
|
+
{ id: 2, name: 'Grace', department: 'Engineering' },
|
|
70
|
+
{ id: 3, name: 'Linus', department: 'Operations' },
|
|
71
|
+
]
|
|
72
|
+
const flatCols = [
|
|
73
|
+
{ field: 'name', header: 'Name', width: 150 },
|
|
74
|
+
{ field: 'department', header: 'Department', width: 180 },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const tree = [
|
|
78
|
+
{ id: 1, managerId: null, name: 'Ada' },
|
|
79
|
+
{ id: 2, managerId: 1, name: 'Grace' },
|
|
80
|
+
{ id: 3, managerId: 2, name: 'Alan' },
|
|
81
|
+
{ id: 4, managerId: 1, name: 'Linus' },
|
|
82
|
+
]
|
|
83
|
+
const treeCols = [{ field: 'name', header: 'Name', width: 200 }]
|
|
84
|
+
const treeData = { parentField: 'managerId', column: 'name' }
|
|
85
|
+
const names = (api: SvGridApi<never, never>) =>
|
|
86
|
+
api.getDisplayedRows().map((r: { name: string }) => r.name)
|
|
87
|
+
|
|
88
|
+
describe('flat grid', () => {
|
|
89
|
+
it('returns every row', async () => {
|
|
90
|
+
const g = mountGrid(flat, flatCols)
|
|
91
|
+
await tick()
|
|
92
|
+
expect(names(g.api)).toEqual(['Ada', 'Grace', 'Linus'])
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('tree data', () => {
|
|
97
|
+
it('includes parent rows once expanded - they are real data rows', async () => {
|
|
98
|
+
const g = mountGrid(tree, treeCols, { treeData })
|
|
99
|
+
await tick()
|
|
100
|
+
g.api.expandAllGroups()
|
|
101
|
+
await tick()
|
|
102
|
+
// Every row the body paints, parents included.
|
|
103
|
+
expect(names(g.api)).toEqual(['Ada', 'Grace', 'Alan', 'Linus'])
|
|
104
|
+
expect(g.target.querySelectorAll('tbody tr').length).toBe(4)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('tracks collapse - a hidden branch is not "displayed"', async () => {
|
|
108
|
+
const g = mountGrid(tree, treeCols, { treeData, getRowId: (r: { id: number }) => String(r.id) })
|
|
109
|
+
await tick()
|
|
110
|
+
// Open Ada only; Grace's own child stays hidden.
|
|
111
|
+
g.api.setRowExpanded('1', true)
|
|
112
|
+
await tick()
|
|
113
|
+
expect(names(g.api)).toEqual(['Ada', 'Grace', 'Linus'])
|
|
114
|
+
expect(g.target.querySelectorAll('tbody tr').length).toBe(3)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('matches the rendered row count at every expansion level', async () => {
|
|
118
|
+
const g = mountGrid(tree, treeCols, { treeData })
|
|
119
|
+
await tick()
|
|
120
|
+
expect(g.api.getDisplayedRows()).toHaveLength(g.target.querySelectorAll('tbody tr').length)
|
|
121
|
+
g.api.expandAllGroups()
|
|
122
|
+
await tick()
|
|
123
|
+
expect(g.api.getDisplayedRows()).toHaveLength(g.target.querySelectorAll('tbody tr').length)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('grouping', () => {
|
|
128
|
+
it('still excludes synthetic group banners', async () => {
|
|
129
|
+
const g = mountGrid(flat, flatCols, { groupBy: ['department'] })
|
|
130
|
+
await tick()
|
|
131
|
+
g.api.expandAllGroups()
|
|
132
|
+
await tick()
|
|
133
|
+
// 2 banners + 3 leaves rendered, but only the 3 leaves are data rows.
|
|
134
|
+
expect(g.target.querySelectorAll('tbody tr').length).toBe(5)
|
|
135
|
+
expect(names(g.api)).toEqual(['Ada', 'Grace', 'Linus'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('returns nothing while every group is collapsed', async () => {
|
|
139
|
+
const g = mountGrid(flat, flatCols, { groupBy: ['department'] })
|
|
140
|
+
await tick()
|
|
141
|
+
expect(g.api.getDisplayedRows()).toEqual([])
|
|
142
|
+
expect(g.target.querySelectorAll('tbody tr').length).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
})
|