@svgrid/grid 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +116 -88
  2. package/README.md +199 -199
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +181 -181
  5. package/dist/SvAutoComplete.svelte +169 -169
  6. package/dist/SvAvatar.svelte +75 -75
  7. package/dist/SvCalendar.svelte +503 -503
  8. package/dist/SvCarousel.svelte +141 -141
  9. package/dist/SvCheckBox.svelte +102 -102
  10. package/dist/SvCircularProgress.svelte +109 -109
  11. package/dist/SvColorInput.svelte +181 -181
  12. package/dist/SvComboBox.svelte +279 -279
  13. package/dist/SvContextMenu.svelte +116 -116
  14. package/dist/SvCountryInput.svelte +163 -163
  15. package/dist/SvDrawer.svelte +254 -254
  16. package/dist/SvDropDownList.svelte +378 -378
  17. package/dist/SvDurationInput.svelte +126 -126
  18. package/dist/SvField.svelte +293 -293
  19. package/dist/SvForm.svelte +437 -437
  20. package/dist/SvGridCellEditor.svelte +748 -744
  21. package/dist/SvGridChart.svelte +1716 -1716
  22. package/dist/SvGridChartPanel.svelte +485 -485
  23. package/dist/SvGridChartView.svelte +70 -70
  24. package/dist/SvGridDropdown.svelte +728 -728
  25. package/dist/SvGridSelect.svelte +270 -270
  26. package/dist/SvGroupCell.svelte +116 -116
  27. package/dist/SvListBox.svelte +334 -334
  28. package/dist/SvMaskedInput.svelte +122 -122
  29. package/dist/SvMenu.svelte +124 -124
  30. package/dist/SvMenuList.svelte +146 -146
  31. package/dist/SvMultiSelect.svelte +293 -293
  32. package/dist/SvNumberInput.svelte +172 -172
  33. package/dist/SvOtpInput.svelte +158 -158
  34. package/dist/SvPasswordInput.svelte +151 -151
  35. package/dist/SvPhoneInput.svelte +133 -133
  36. package/dist/SvPopover.svelte +197 -197
  37. package/dist/SvProgress.svelte +116 -116
  38. package/dist/SvRadioGroup.svelte +107 -107
  39. package/dist/SvRating.svelte +112 -112
  40. package/dist/SvResult.svelte +73 -73
  41. package/dist/SvRichText.svelte +211 -211
  42. package/dist/SvRowGroupPanel.svelte +170 -170
  43. package/dist/SvScrollArea.svelte +61 -61
  44. package/dist/SvSlider.svelte +203 -203
  45. package/dist/SvSwitchButton.svelte +108 -108
  46. package/dist/SvTagsInput.svelte +115 -115
  47. package/dist/SvTextInput.svelte +147 -147
  48. package/dist/SvTimePicker.svelte +245 -245
  49. package/dist/SvToaster.svelte +159 -159
  50. package/dist/SvToggleButton.svelte +85 -85
  51. package/dist/SvTooltip.svelte +161 -161
  52. package/dist/SvTour.svelte +208 -208
  53. package/dist/SvTree.svelte +444 -444
  54. package/dist/SvTreeSelect.svelte +239 -239
  55. package/dist/cdn/{GridMenus-DMihUtma.js → GridMenus-CYzOvmNQ.js} +3 -3
  56. package/dist/cdn/{GridMenus-nmNDj1a3.js → GridMenus-DFB6iwF8.js} +3 -3
  57. package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
  58. package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
  59. package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
  60. package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
  61. package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
  62. package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
  63. package/dist/cdn/{SvGridChart-CwhFz7GV.js → SvGridChart-BEJmNNx9.js} +1 -1
  64. package/dist/cdn/{SvGridChart-Bs2GIR2Q.js → SvGridChart-BwVos976.js} +1 -1
  65. package/dist/cdn/{SvGridChartPanel-1WXuStzM.js → SvGridChartPanel-BOknOkrP.js} +1 -1
  66. package/dist/cdn/{SvGridChartPanel-e5_nqNP9.js → SvGridChartPanel-D2PjII4O.js} +1 -1
  67. package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-00LPUHL1.js} +1 -1
  68. package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-BhUEJ5ki.js} +1 -1
  69. package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
  70. package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
  71. package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
  72. package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
  73. package/dist/cdn/{src-B_YS5AOc.js → src-C8QjSaq-.js} +42 -44
  74. package/dist/cdn/{src-DYXXpuSk.js → src-D0hO2eYN.js} +41 -43
  75. package/dist/cdn/svgrid.js +7 -7
  76. package/dist/cdn/svgrid.svelte-external.js +7 -7
  77. package/dist/chart-export.js +8 -8
  78. package/dist/editing.js +22 -13
  79. package/package.json +11 -11
  80. package/src/FlexRender.svelte +96 -96
  81. package/src/GridFooter.svelte +181 -181
  82. package/src/SvAutoComplete.svelte +169 -169
  83. package/src/SvAvatar.svelte +75 -75
  84. package/src/SvCalendar.svelte +503 -503
  85. package/src/SvCalendar.test.ts +226 -226
  86. package/src/SvCarousel.svelte +141 -141
  87. package/src/SvCheckBox.svelte +102 -102
  88. package/src/SvCircularProgress.svelte +109 -109
  89. package/src/SvColorInput.svelte +181 -181
  90. package/src/SvComboBox.svelte +279 -279
  91. package/src/SvContextMenu.svelte +116 -116
  92. package/src/SvCountryInput.svelte +163 -163
  93. package/src/SvDrawer.svelte +254 -254
  94. package/src/SvDropDownList.svelte +378 -378
  95. package/src/SvDurationInput.svelte +126 -126
  96. package/src/SvField.svelte +293 -293
  97. package/src/SvForm.svelte +437 -437
  98. package/src/SvForm.test.ts +411 -411
  99. package/src/SvGrid.types.ts +2092 -2092
  100. package/src/SvGridCellEditor.svelte +748 -744
  101. package/src/SvGridChart.svelte +1716 -1716
  102. package/src/SvGridChartPanel.svelte +485 -485
  103. package/src/SvGridChartView.svelte +70 -70
  104. package/src/SvGridDropdown.svelte +728 -728
  105. package/src/SvGridSelect.svelte +270 -270
  106. package/src/SvGroupCell.svelte +116 -116
  107. package/src/SvListBox.svelte +334 -334
  108. package/src/SvMaskedInput.svelte +122 -122
  109. package/src/SvMenu.svelte +124 -124
  110. package/src/SvMenu.test.ts +97 -97
  111. package/src/SvMenuList.svelte +146 -146
  112. package/src/SvMultiSelect.svelte +293 -293
  113. package/src/SvNumberInput.svelte +172 -172
  114. package/src/SvOtpInput.svelte +158 -158
  115. package/src/SvPasswordInput.svelte +151 -151
  116. package/src/SvPhoneInput.svelte +133 -133
  117. package/src/SvPopover.svelte +197 -197
  118. package/src/SvProgress.svelte +116 -116
  119. package/src/SvRadioGroup.svelte +107 -107
  120. package/src/SvRating.svelte +112 -112
  121. package/src/SvResult.svelte +73 -73
  122. package/src/SvRichText.svelte +211 -211
  123. package/src/SvRowGroupPanel.svelte +170 -170
  124. package/src/SvScrollArea.svelte +61 -61
  125. package/src/SvSlider.svelte +203 -203
  126. package/src/SvSwitchButton.svelte +108 -108
  127. package/src/SvTagsInput.svelte +115 -115
  128. package/src/SvTextInput.svelte +147 -147
  129. package/src/SvTimePicker.svelte +245 -245
  130. package/src/SvToaster.svelte +159 -159
  131. package/src/SvToaster.test.ts +95 -95
  132. package/src/SvToggleButton.svelte +85 -85
  133. package/src/SvTooltip.svelte +161 -161
  134. package/src/SvTour.svelte +208 -208
  135. package/src/SvTree.svelte +444 -444
  136. package/src/SvTreeSelect.svelte +239 -239
  137. package/src/a11y/dismissable.test.ts +119 -119
  138. package/src/a11y/dismissable.ts +114 -114
  139. package/src/a11y.contract.test.ts +49 -49
  140. package/src/a11y.test.ts +59 -59
  141. package/src/a11y.ts +61 -61
  142. package/src/ai.test.ts +502 -502
  143. package/src/ai.ts +1419 -1419
  144. package/src/build-api.coverage.test.ts +633 -633
  145. package/src/build-api.ts +846 -846
  146. package/src/builtin-editors.grid.test.ts +83 -83
  147. package/src/cell-formatting.ts +171 -171
  148. package/src/cell-render.test.ts +513 -513
  149. package/src/cell-render.ts +496 -496
  150. package/src/cell-values.ts +148 -148
  151. package/src/chart-export.ts +202 -202
  152. package/src/chart-view.svelte.ts +36 -36
  153. package/src/chart.ts +2321 -2321
  154. package/src/collaboration.test.ts +104 -104
  155. package/src/collaboration.ts +167 -167
  156. package/src/column-groups.ts +78 -78
  157. package/src/core.performance.test.ts +30 -30
  158. package/src/core.ts +1865 -1865
  159. package/src/createAutocomplete.svelte.ts +132 -132
  160. package/src/createCombobox.svelte.ts +191 -191
  161. package/src/createCountryInput.svelte.ts +157 -157
  162. package/src/createDropdownList.svelte.ts +168 -168
  163. package/src/createForm.svelte.ts +386 -386
  164. package/src/createGrid.svelte.ts +42 -42
  165. package/src/createGrid.test.ts +10 -10
  166. package/src/createGridState.svelte.ts +17 -17
  167. package/src/createListbox.svelte.ts +250 -250
  168. package/src/createMenu.svelte.ts +224 -224
  169. package/src/createPopoverSelect.svelte.ts +213 -213
  170. package/src/createSlider.svelte.ts +191 -191
  171. package/src/createTooltip.svelte.ts +144 -144
  172. package/src/createTree.svelte.ts +322 -322
  173. package/src/datetime/date-core.ts +206 -206
  174. package/src/datetime/date-restrict.ts +61 -61
  175. package/src/datetime/timezone.ts +135 -135
  176. package/src/dock-manager-model.ts +596 -596
  177. package/src/dock-model.ts +374 -374
  178. package/src/editing.test.ts +974 -974
  179. package/src/editing.ts +618 -609
  180. package/src/editor-contract.ts +171 -171
  181. package/src/editor-registry.grid.test.ts +144 -144
  182. package/src/editor-registry.ts +122 -122
  183. package/src/export-data-api.test.ts +126 -126
  184. package/src/export-format.test.ts +107 -107
  185. package/src/export-format.ts +601 -601
  186. package/src/filter-operators.ts +160 -160
  187. package/src/filtering/excel-filters.ts +325 -325
  188. package/src/flex-render.ts +3 -3
  189. package/src/form-field.ts +127 -127
  190. package/src/group-display.test.ts +167 -167
  191. package/src/group-display.ts +200 -200
  192. package/src/headless.ts +87 -87
  193. package/src/js-scroller.svelte.ts +173 -173
  194. package/src/keyboard-handlers.ts +270 -270
  195. package/src/keyboard.test.ts +59 -59
  196. package/src/keyboard.ts +97 -97
  197. package/src/list-nav.test.ts +49 -49
  198. package/src/list-nav.ts +29 -29
  199. package/src/list-option.test.ts +56 -56
  200. package/src/list-option.ts +179 -179
  201. package/src/menus.ts +597 -597
  202. package/src/merge-objects.ts +48 -48
  203. package/src/overlays.test.ts +90 -90
  204. package/src/positioning.ts +268 -268
  205. package/src/render-component.ts +28 -28
  206. package/src/row-drag.test.ts +401 -401
  207. package/src/row-drag.ts +419 -419
  208. package/src/row-resize.test.ts +524 -524
  209. package/src/row-resize.ts +228 -228
  210. package/src/scheduler-ical.ts +181 -181
  211. package/src/scheduler-model.test.ts +562 -562
  212. package/src/scheduler-model.ts +873 -873
  213. package/src/selection.test.ts +885 -885
  214. package/src/server-data-source.test.ts +383 -383
  215. package/src/server-data-source.ts +469 -469
  216. package/src/sparkline.test.ts +68 -68
  217. package/src/sparkline.ts +169 -169
  218. package/src/spreadsheet.test.ts +488 -488
  219. package/src/spreadsheet.ts +312 -312
  220. package/src/static-functions.ts +11 -11
  221. package/src/subscribe.ts +38 -38
  222. package/src/summaries.ts +113 -113
  223. package/src/svgrid-wrapper.types.ts +563 -563
  224. package/src/svgrid.async-editor-options.test.ts +273 -273
  225. package/src/svgrid.auto-row-height.test.ts +204 -204
  226. package/src/svgrid.behavior.test.ts +910 -910
  227. package/src/svgrid.charting.test.ts +534 -534
  228. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  229. package/src/svgrid.context-menu.test.ts +147 -147
  230. package/src/svgrid.features.test.ts +157 -157
  231. package/src/svgrid.filter-depth.test.ts +163 -163
  232. package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
  233. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  234. package/src/svgrid.grand-total.test.ts +188 -188
  235. package/src/svgrid.group-display-mode.test.ts +171 -171
  236. package/src/svgrid.group-footers.test.ts +121 -121
  237. package/src/svgrid.group-pagination.test.ts +153 -153
  238. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  239. package/src/svgrid.tree-data.test.ts +186 -186
  240. package/src/svgrid.wrapper.test.ts +63 -63
  241. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  242. package/src/test-setup.ts +62 -62
  243. package/src/themes/index.ts +288 -288
  244. package/src/toast-store.svelte.ts +250 -250
  245. package/src/toast-store.test.ts +147 -147
  246. package/src/tree-row-model.test.ts +168 -168
  247. package/src/ui-buttons.test.ts +144 -144
  248. package/src/ui-inputs.test.ts +118 -118
  249. package/src/ui-localization.test.ts +113 -113
  250. package/src/ui-range.test.ts +70 -70
  251. package/src/ui-selection.test.ts +155 -155
  252. package/src/ui-tier1.test.ts +142 -142
  253. package/src/virtual.test.ts +88 -88
  254. package/src/virtualization/column-virtualizer.test.ts +27 -27
  255. package/src/virtualization/column-virtualizer.ts +30 -30
  256. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  257. package/src/virtualization/types.ts +30 -30
  258. package/src/virtualization/virtualizer.test.ts +47 -47
  259. package/src/virtualization/virtualizer.ts +322 -322
@@ -1,913 +1,913 @@
1
- /**
2
- * Broad behavioral test suite for the <SvGrid /> render component.
3
- *
4
- * The intent is wide coverage on a single file - SvGrid.svelte is ~3000
5
- * lines and most of its branches are reachable only when the component is
6
- * mounted with a particular prop combination. Each test below exercises a
7
- * different feature set so the whole file's effect graph is traversed.
8
- *
9
- * Pattern (re-used across files):
10
- * 1. mount SvGrid in jsdom with the props that trigger the feature
11
- * 2. wait one microtask for $effects to flush
12
- * 3. assert on the api object delivered via onApiReady, or on visible DOM
13
- * 4. unmount
14
- */
15
-
16
- import { describe, expect, it } from 'vitest'
17
- import { mount, unmount } from 'svelte'
18
- import SvGrid from './SvGrid.svelte'
19
- import {
20
- columnFilteringFeature,
21
- columnGroupingFeature,
22
- createCoreRowModel,
23
- createExpandedRowModel,
24
- createFilteredRowModel,
25
- createGroupedRowModel,
26
- createPaginatedRowModel,
27
- createSortedRowModel,
28
- rowExpandingFeature,
29
- rowPaginationFeature,
30
- rowSelectionFeature,
31
- rowSortingFeature,
32
- sortFns,
33
- tableFeatures,
34
- } from './index'
35
- import type { ColumnDef, SvGridApi } from './index'
36
-
37
- type Person = {
38
- id: number
39
- name: string
40
- team: string
41
- age: number
42
- active: boolean
43
- joined: string
44
- salary: number
45
- }
46
-
47
- const fullFeatures = tableFeatures({
48
- columnFilteringFeature,
49
- columnGroupingFeature,
50
- rowExpandingFeature,
51
- rowPaginationFeature,
52
- rowSelectionFeature,
53
- rowSortingFeature,
54
- })
55
-
56
- const people: Person[] = [
57
- { id: 1, name: 'Ada Lovelace', team: 'Research', age: 38, active: true, joined: '2020-01-10', salary: 142_000 },
58
- { id: 2, name: 'Grace Hopper', team: 'Compilers', age: 42, active: true, joined: '2018-05-22', salary: 158_000 },
59
- { id: 3, name: 'Alan Turing', team: 'Research', age: 41, active: false, joined: '2021-09-01', salary: 138_000 },
60
- { id: 4, name: 'Margaret Hamilton', team: 'Apollo', age: 35, active: true, joined: '2019-03-15', salary: 165_000 },
61
- { id: 5, name: 'Linus Torvalds', team: 'Kernel', age: 54, active: true, joined: '2017-11-30', salary: 175_000 },
62
- { id: 6, name: 'Donald Knuth', team: 'Research', age: 86, active: false, joined: '2010-06-06', salary: 120_000 },
63
- { id: 7, name: 'Brian Kernighan', team: 'Compilers', age: 80, active: true, joined: '2012-02-02', salary: 160_000 },
64
- { id: 8, name: 'Dennis Ritchie', team: 'Kernel', age: 70, active: false, joined: '2009-07-14', salary: 110_000 },
65
- ]
66
-
67
- const personColumns: ColumnDef<typeof fullFeatures, Person>[] = [
68
- { field: 'name', header: 'Name', width: 200 },
69
- { field: 'team', header: 'Team', width: 160 },
70
- {
71
- field: 'age',
72
- header: 'Age',
73
- width: 100,
74
- editorType: 'number',
75
- format: { type: 'number' },
76
- },
77
- {
78
- field: 'active',
79
- header: 'Active',
80
- width: 100,
81
- editorType: 'checkbox',
82
- },
83
- {
84
- field: 'joined',
85
- header: 'Joined',
86
- width: 140,
87
- editorType: 'date',
88
- format: { type: 'number' },
89
- },
90
- {
91
- field: 'salary',
92
- header: 'Salary',
93
- width: 140,
94
- editorType: 'number',
95
- format: { type: 'currency', currency: 'USD' },
96
- },
97
- ]
98
-
99
- type MountResult = {
100
- api: SvGridApi<typeof fullFeatures, Person>
101
- target: HTMLElement
102
- destroy: () => void
103
- }
104
-
105
- function mountGrid(overrides: Record<string, unknown> = {}): Promise<MountResult> {
106
- return new Promise((resolveApi, rejectApi) => {
107
- const target = document.createElement('div')
108
- target.style.width = '1200px'
109
- target.style.height = '600px'
110
- document.body.appendChild(target)
111
-
112
- let capturedApi: SvGridApi<typeof fullFeatures, Person> | null = null
113
-
114
- const app = mount(SvGrid, {
115
- target,
116
- props: {
117
- data: people,
118
- columns: personColumns,
119
- features: fullFeatures,
120
- _rowModels: {
121
- coreRowModel: createCoreRowModel(),
122
- filteredRowModel: createFilteredRowModel(),
123
- sortedRowModel: createSortedRowModel(sortFns),
124
- groupedRowModel: createGroupedRowModel(),
125
- expandedRowModel: createExpandedRowModel(),
126
- paginatedRowModel: createPaginatedRowModel(),
127
- },
128
- rowHeight: 36,
129
- containerHeight: 480,
130
- virtualization: false,
131
- onApiReady(api: SvGridApi<typeof fullFeatures, Person>) {
132
- capturedApi = api
133
- resolveApi({
134
- api,
135
- target,
136
- destroy: () => {
137
- unmount(app)
138
- target.remove()
139
- },
140
- })
141
- },
142
- ...overrides,
143
- } as any,
144
- })
145
-
146
- queueMicrotask(() => {
147
- if (!capturedApi) rejectApi(new Error('onApiReady never fired'))
148
- })
149
- })
150
- }
151
-
152
- const tick = () => Promise.resolve()
153
-
154
- describe('SvGrid - core render', () => {
155
- it('mounts and renders the column headers', async () => {
156
- const { target, destroy } = await mountGrid()
157
- try {
158
- const headers = target.querySelectorAll('[data-svgrid-header-col]')
159
- // jsdom reports 0 for offsetWidth so column virtualization may render
160
- // a subset. The point of this assertion is just that the header row
161
- // exists and is populated.
162
- expect(headers.length).toBeGreaterThan(0)
163
- } finally {
164
- destroy()
165
- }
166
- })
167
-
168
- it('renders the rows in the body', async () => {
169
- const { target, destroy } = await mountGrid()
170
- try {
171
- await tick()
172
- const rows = target.querySelectorAll('[data-svgrid-row]')
173
- // Pagination defaults to off in our mount - all rows render.
174
- expect(rows.length).toBeGreaterThanOrEqual(people.length)
175
- } finally {
176
- destroy()
177
- }
178
- })
179
-
180
- it('renders the empty message when data is empty', async () => {
181
- const { target, destroy } = await mountGrid({ data: [], emptyMessage: 'No rows yet' })
182
- try {
183
- await tick()
184
- expect(target.textContent).toContain('No rows yet')
185
- } finally {
186
- destroy()
187
- }
188
- })
189
-
190
- it('renders a loading state when `loading` is true', async () => {
191
- const { target, destroy } = await mountGrid({ loading: true })
192
- try {
193
- await tick()
194
- const statusEls = target.querySelectorAll('[role="status"]')
195
- expect(statusEls.length).toBeGreaterThan(0)
196
- } finally {
197
- destroy()
198
- }
199
- })
200
-
201
- it('renders an error state when `error` is set', async () => {
202
- const { target, destroy } = await mountGrid({ error: 'Something broke' })
203
- try {
204
- await tick()
205
- expect(target.textContent).toContain('Something broke')
206
- } finally {
207
- destroy()
208
- }
209
- })
210
- })
211
-
212
- describe('SvGrid - sort', () => {
213
- it('setSort then clearSort updates filters via api', async () => {
214
- const { api, destroy } = await mountGrid()
215
- try {
216
- api.setSort('name', 'asc')
217
- await tick()
218
- api.setSort('name', 'desc')
219
- await tick()
220
- api.setSort('name', null)
221
- await tick()
222
- api.clearSort()
223
- await tick()
224
- // No throw + state consistent = pass.
225
- expect(api.getData().length).toBe(people.length)
226
- } finally {
227
- destroy()
228
- }
229
- })
230
- })
231
-
232
- describe('SvGrid - pagination', () => {
233
- it('shows fewer displayed rows than total when paginated', async () => {
234
- const { api, destroy } = await mountGrid({ showPagination: true, pageSize: 3 })
235
- try {
236
- await tick()
237
- const displayed = api.getDisplayedRows()
238
- expect(displayed.length).toBeLessThanOrEqual(3)
239
- } finally {
240
- destroy()
241
- }
242
- })
243
- })
244
-
245
- describe('SvGrid - global filter', () => {
246
- it('showGlobalFilter renders the input', async () => {
247
- const { target, destroy } = await mountGrid({ showGlobalFilter: true })
248
- try {
249
- await tick()
250
- const input = target.querySelector(
251
- '.sv-grid-global-filter input, input[type="text"]',
252
- )
253
- expect(input).not.toBeNull()
254
- } finally {
255
- destroy()
256
- }
257
- })
258
- })
259
-
260
- describe('SvGrid - column filter row', () => {
261
- it('showColumnFilters renders the filter row inputs', async () => {
262
- const { target, destroy } = await mountGrid({ showColumnFilters: true })
263
- try {
264
- await tick()
265
- const inputs = target.querySelectorAll('.sv-grid-column-filter')
266
- // One filter input per filterable column.
267
- expect(inputs.length).toBeGreaterThan(0)
268
- } finally {
269
- destroy()
270
- }
271
- })
272
- })
273
-
274
- describe('SvGrid - inline editing', () => {
275
- it('enables enableInlineEditing without throwing', async () => {
276
- const { api, destroy } = await mountGrid({ enableInlineEditing: true })
277
- try {
278
- // setCellValue should mutate internal data.
279
- api.setCellValue(0, 'name', 'Renamed Person')
280
- await tick()
281
- const data = api.getData()
282
- expect((data[0] as Person).name).toBe('Renamed Person')
283
- } finally {
284
- destroy()
285
- }
286
- })
287
- })
288
-
289
- describe('SvGrid - row selection', () => {
290
- it('showRowSelection renders the checkbox column', async () => {
291
- const { target, destroy } = await mountGrid({ showRowSelection: true })
292
- try {
293
- await tick()
294
- const checkboxes = target.querySelectorAll('.sv-grid-checkbox')
295
- // At least the header + one per row.
296
- expect(checkboxes.length).toBeGreaterThan(0)
297
- } finally {
298
- destroy()
299
- }
300
- })
301
- })
302
-
303
- describe('SvGrid - capability shortcuts', () => {
304
- it('sortable + filterable enable sorting/filtering UI with no features prop', async () => {
305
- const { target, destroy } = await mountGrid({
306
- features: undefined,
307
- sortable: true,
308
- filterable: true,
309
- })
310
- try {
311
- await tick()
312
- // rowSortingFeature injected -> headers expose aria-sort.
313
- expect(
314
- target.querySelectorAll('[role="columnheader"][aria-sort]').length,
315
- ).toBeGreaterThan(0)
316
- // columnFilteringFeature injected -> per-column filter button renders.
317
- expect(target.querySelector('.sv-grid-col-filter-btn')).toBeTruthy()
318
- } finally {
319
- destroy()
320
- }
321
- })
322
-
323
- it('sortable={false} overrides a rowSortingFeature passed via features', async () => {
324
- // The harness default `fullFeatures` includes rowSortingFeature.
325
- const { target, destroy } = await mountGrid({ sortable: false })
326
- try {
327
- await tick()
328
- expect(
329
- target.querySelector('[role="columnheader"][aria-sort]'),
330
- ).toBeNull()
331
- } finally {
332
- destroy()
333
- }
334
- })
335
-
336
- it('pageable toggles the pagination footer', async () => {
337
- const off = await mountGrid({ pageable: false })
338
- try {
339
- await tick()
340
- expect(off.target.querySelector('.sv-grid-pagination')).toBeNull()
341
- } finally {
342
- off.destroy()
343
- }
344
- const on = await mountGrid({ features: undefined, pageable: true })
345
- try {
346
- await tick()
347
- expect(on.target.querySelector('.sv-grid-pagination')).toBeTruthy()
348
- } finally {
349
- on.destroy()
350
- }
351
- })
352
-
353
- it('paginationPosition mounts + gates the bottom pager', async () => {
354
- // The top pager is gated on measurement (jsdom does not measure), so it's not
355
- // asserted here; the visible top pager is covered by the browser/e2e check.
356
- // This guards the mount path (a top footer rendered pre-measure used to crash).
357
- for (const paginationPosition of ['top', 'both', 'bottom'] as const) {
358
- const g = await mountGrid({ showPagination: true, paginationPosition })
359
- try {
360
- await tick()
361
- expect(g.api).toBeTruthy()
362
- const bottoms = [...g.target.querySelectorAll('.sv-grid-pagination')].filter((el) => !el.classList.contains('sv-grid-pagination-top'))
363
- expect(bottoms.length).toBe(paginationPosition === 'top' ? 0 : 1) // no bottom pager when position is 'top'
364
- } finally {
365
- g.destroy()
366
- }
367
- }
368
- })
369
-
370
- it('pageSizeOptions is accepted by the pager', async () => {
371
- const g = await mountGrid({ showPagination: true, pageSizeOptions: [5, 15, 45] })
372
- try {
373
- await tick()
374
- expect(g.target.querySelector('.sv-grid-pagination')).toBeTruthy()
375
- } finally {
376
- g.destroy()
377
- }
378
- })
379
-
380
- it('editable={false} blocks inline editing entry points', async () => {
381
- const { api, target, destroy } = await mountGrid({ editable: false })
382
- try {
383
- await tick()
384
- // Double-clicking a body cell must not open an editor when editing
385
- // is disabled via the shortcut.
386
- const cell = target.querySelector<HTMLElement>('.sv-grid-cell')
387
- cell?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
388
- await tick()
389
- expect(target.querySelector('.sv-grid-cell-editor')).toBeNull()
390
- expect(api.getData().length).toBe(people.length)
391
- } finally {
392
- destroy()
393
- }
394
- })
395
- })
396
-
397
- describe('SvGrid - cell selection', () => {
398
- it('enableCellSelection mounts without error', async () => {
399
- const { api, destroy } = await mountGrid({ enableCellSelection: true })
400
- try {
401
- await tick()
402
- expect(api.getData().length).toBe(people.length)
403
- } finally {
404
- destroy()
405
- }
406
- })
407
- })
408
-
409
- describe('SvGrid - grouping + aggregation', () => {
410
- it('setGroupBy mounts + the codepath runs without throwing', async () => {
411
- const { api, target, destroy } = await mountGrid({
412
- enableRowSummaries: true,
413
- showGroupingControls: true,
414
- })
415
- try {
416
- api.setGroupBy(['team'])
417
- await tick()
418
- await tick()
419
- // Sanity: the grid is still mounted and the API responsive.
420
- const data = api.getData()
421
- expect(data.length).toBe(people.length)
422
- // Group rendering depends on row model + container dimensions in jsdom;
423
- // just verify the codepath does not throw.
424
- expect(target).toBeTruthy()
425
- } finally {
426
- destroy()
427
- }
428
- })
429
- })
430
-
431
- describe('SvGrid - column add / remove / visibility', () => {
432
- it('addColumn and removeColumn round-trip', async () => {
433
- const { api, destroy } = await mountGrid()
434
- try {
435
- api.addColumn({ id: 'id-col', field: 'id', header: 'ID', width: 60 } as any, 'left')
436
- await tick()
437
- api.addColumn(
438
- { id: 'extra-tag', field: 'team', header: 'Extra', width: 60 } as any,
439
- 'right',
440
- )
441
- await tick()
442
- api.addColumn(
443
- { id: 'mid-col', field: 'name', header: 'Mid', width: 60 } as any,
444
- 1,
445
- )
446
- await tick()
447
- api.removeColumn('id-col')
448
- await tick()
449
- // No throw means the codepath was exercised.
450
- expect(typeof api.getData).toBe('function')
451
- } finally {
452
- destroy()
453
- }
454
- })
455
-
456
- it('setColumnVisible toggles visibility flag', async () => {
457
- const { api, destroy } = await mountGrid()
458
- try {
459
- expect(api.isColumnVisible('name')).toBe(true)
460
- api.setColumnVisible('name', false)
461
- await tick()
462
- expect(api.isColumnVisible('name')).toBe(false)
463
- api.setColumnVisible('name', true)
464
- await tick()
465
- expect(api.isColumnVisible('name')).toBe(true)
466
- } finally {
467
- destroy()
468
- }
469
- })
470
-
471
- it('columns marked `visible: false` start hidden but stay listed', async () => {
472
- const columns: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map(
473
- (c) => (c.field === 'team' ? { ...c, visible: false } : c),
474
- )
475
- const { api, destroy } = await mountGrid({ columns })
476
- try {
477
- // Starts hidden...
478
- expect(api.isColumnVisible('team')).toBe(false)
479
- // ...but is still listed in getColumns() (so a Choose Columns UI
480
- // can offer it), just with visible=false.
481
- const team = api.getColumns().find((c) => c.id === 'team')
482
- expect(team).toBeDefined()
483
- expect(team?.visible).toBe(false)
484
- // A later user toggle wins over the initial flag.
485
- api.setColumnVisible('team', true)
486
- await tick()
487
- expect(api.isColumnVisible('team')).toBe(true)
488
- } finally {
489
- destroy()
490
- }
491
- })
492
- })
493
-
494
- describe('SvGrid - row add / remove', () => {
495
- it('addRow at top / bottom / specific index', async () => {
496
- const { api, destroy } = await mountGrid()
497
- try {
498
- const before = api.getData().length
499
- api.addRow({ id: 99, name: 'Top', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'top')
500
- api.addRow({ id: 100, name: 'Bottom', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'bottom')
501
- api.addRow({ id: 101, name: 'Mid', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 2)
502
- await tick()
503
- expect(api.getData().length).toBe(before + 3)
504
- } finally {
505
- destroy()
506
- }
507
- })
508
-
509
- it('addRows / removeRow / removeRows', async () => {
510
- const { api, destroy } = await mountGrid()
511
- try {
512
- const before = api.getData().length
513
- api.addRows(
514
- [
515
- { id: 90, name: 'A', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
516
- { id: 91, name: 'B', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
517
- ],
518
- 'bottom',
519
- )
520
- await tick()
521
- api.removeRow(0)
522
- await tick()
523
- api.removeRows([0, 1])
524
- await tick()
525
- expect(api.getData().length).toBe(before + 2 - 3)
526
- } finally {
527
- destroy()
528
- }
529
- })
530
- })
531
-
532
- describe('SvGrid - getCellValue', () => {
533
- it('returns the cell value for a known row+column', async () => {
534
- const { api, destroy } = await mountGrid()
535
- try {
536
- const value = api.getCellValue(0, 'name')
537
- expect(value).toBe(people[0]!.name)
538
- } finally {
539
- destroy()
540
- }
541
- })
542
-
543
- it('returns undefined for unknown row or column', async () => {
544
- const { api, destroy } = await mountGrid()
545
- try {
546
- expect(api.getCellValue(999, 'name')).toBeUndefined()
547
- expect(api.getCellValue(0, 'no-such-col')).toBeUndefined()
548
- } finally {
549
- destroy()
550
- }
551
- })
552
- })
553
-
554
- describe('SvGrid - row number column', () => {
555
- it('showRowNumbers renders the leading row-number column', async () => {
556
- const { target, destroy } = await mountGrid({ showRowNumbers: true })
557
- try {
558
- await tick()
559
- const rnCells = target.querySelectorAll('.sv-grid-row-number-cell')
560
- expect(rnCells.length).toBeGreaterThan(0)
561
- } finally {
562
- destroy()
563
- }
564
- })
565
- })
566
-
567
- describe('SvGrid - external sort / filter', () => {
568
- it('externalSort + onSortingChange fires without re-ordering rows', async () => {
569
- let captured: Array<{ id: string; desc: boolean }> | null = null
570
- const { api, destroy } = await mountGrid({
571
- externalSort: true,
572
- onSortingChange: (s: any) => (captured = s),
573
- })
574
- try {
575
- api.setSort('age', 'desc')
576
- await tick()
577
- expect(captured).not.toBeNull()
578
- } finally {
579
- destroy()
580
- }
581
- })
582
-
583
- it('externalFilter + onFiltersChange fires when a filter is set', async () => {
584
- let calls = 0
585
- const { api, destroy } = await mountGrid({
586
- externalFilter: true,
587
- onFiltersChange: () => {
588
- calls += 1
589
- },
590
- })
591
- try {
592
- api.setFilter('team', { operator: 'contains', value: 'Research' })
593
- await tick()
594
- expect(calls).toBeGreaterThan(0)
595
- } finally {
596
- destroy()
597
- }
598
- })
599
- })
600
-
601
- describe('SvGrid - custom snippet cell', () => {
602
- it('accepts a custom cell renderer without throwing', async () => {
603
- // Build a snippet via raw object - the SvGrid component reads
604
- // `column.columnDef.cell` and pipes it through FlexRender. Returning a
605
- // plain string is the simplest renderer; tests only need to verify the
606
- // render path doesn't crash.
607
- const customCols: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map((c) =>
608
- c.field === 'name'
609
- ? {
610
- ...c,
611
- cell: (ctx: any) => `★ ${(ctx.row.original as Person).name}`,
612
- }
613
- : c,
614
- )
615
- const { target, destroy } = await mountGrid({ columns: customCols })
616
- try {
617
- await tick()
618
- expect(target.textContent).toContain('★')
619
- } finally {
620
- destroy()
621
- }
622
- })
623
- })
624
-
625
- describe('SvGrid - filterMode shortcuts', () => {
626
- it('filterMode="row" + global filter input wiring', async () => {
627
- const { destroy } = await mountGrid({ filterMode: 'row' })
628
- try {
629
- await tick()
630
- // No assertion needed - exercising the codepath is enough.
631
- expect(true).toBe(true)
632
- } finally {
633
- destroy()
634
- }
635
- })
636
- it('filterMode="global"', async () => {
637
- const { destroy } = await mountGrid({ filterMode: 'global' })
638
- try {
639
- await tick()
640
- expect(true).toBe(true)
641
- } finally {
642
- destroy()
643
- }
644
- })
645
- it('filterMode="none"', async () => {
646
- const { destroy } = await mountGrid({ filterMode: 'none' })
647
- try {
648
- await tick()
649
- expect(true).toBe(true)
650
- } finally {
651
- destroy()
652
- }
653
- })
654
- })
655
-
656
- describe('SvGrid - selectionMode shortcuts', () => {
657
- it('selectionMode="row"', async () => {
658
- const { destroy } = await mountGrid({ selectionMode: 'row' })
659
- try {
660
- await tick()
661
- expect(true).toBe(true)
662
- } finally {
663
- destroy()
664
- }
665
- })
666
- it('selectionMode="cell"', async () => {
667
- const { destroy } = await mountGrid({ selectionMode: 'cell' })
668
- try {
669
- await tick()
670
- expect(true).toBe(true)
671
- } finally {
672
- destroy()
673
- }
674
- })
675
- it('selectionMode="both"', async () => {
676
- const { destroy } = await mountGrid({ selectionMode: 'both' })
677
- try {
678
- await tick()
679
- expect(true).toBe(true)
680
- } finally {
681
- destroy()
682
- }
683
- })
684
- it('selectionMode="none"', async () => {
685
- const { destroy } = await mountGrid({ selectionMode: 'none' })
686
- try {
687
- await tick()
688
- expect(true).toBe(true)
689
- } finally {
690
- destroy()
691
- }
692
- })
693
- })
694
-
695
- describe('SvGrid - fitColumns', () => {
696
- it('mounts with fitColumns=true without throwing', async () => {
697
- const { destroy } = await mountGrid({ fitColumns: true })
698
- try {
699
- await tick()
700
- expect(true).toBe(true)
701
- } finally {
702
- destroy()
703
- }
704
- })
705
- })
706
-
707
- describe('SvGrid - selectable shortcut (cell selection)', () => {
708
- // The gated path a user actually takes: pointerdown on a cell runs through
709
- // createSelection.setSelection, which bails on !enableCellSelectionEffective.
710
- // Asserting through it (rather than api.selectCells, which writes the range
711
- // directly and bypasses the gate) is what makes these tests meaningful.
712
- // Deliberately skips `.sv-grid-selection-cell`: the checkbox column is a
713
- // different surface and a pointerdown there starts no range, so clicking it
714
- // would make every assertion below pass vacuously.
715
- async function clickCell(target: HTMLElement, index: number) {
716
- const cells = target.querySelectorAll(
717
- '.sv-grid-cell:not(.sv-grid-selection-cell)',
718
- )
719
- const cell = cells[index]
720
- if (!cell) throw new Error(`no data cell at index ${index} (found ${cells.length})`)
721
- cell.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
722
- await tick()
723
- }
724
-
725
- it('is ON by default - selectionMode defaults to "both"', async () => {
726
- const { api, target, destroy } = await mountGrid()
727
- try {
728
- await tick()
729
- await clickCell(target, 0)
730
- expect(api.getSelected().length).toBe(1)
731
- } finally {
732
- destroy()
733
- }
734
- })
735
-
736
- it('selectable={false} turns cell selection off', async () => {
737
- const { api, target, destroy } = await mountGrid({ selectable: false })
738
- try {
739
- await tick()
740
- await clickCell(target, 0)
741
- expect(api.getSelected()).toEqual([])
742
- } finally {
743
- destroy()
744
- }
745
- })
746
-
747
- it('selectable={true} wins over selectionMode="none"', async () => {
748
- const { api, target, destroy } = await mountGrid({
749
- selectable: true,
750
- selectionMode: 'none',
751
- })
752
- try {
753
- await tick()
754
- await clickCell(target, 0)
755
- expect(api.getSelected().length).toBe(1)
756
- } finally {
757
- destroy()
758
- }
759
- })
760
-
761
- it('selectable={false} wins over enableCellSelection={true}', async () => {
762
- const { api, target, destroy } = await mountGrid({
763
- selectable: false,
764
- enableCellSelection: true,
765
- })
766
- try {
767
- await tick()
768
- await clickCell(target, 0)
769
- expect(api.getSelected()).toEqual([])
770
- } finally {
771
- destroy()
772
- }
773
- })
774
-
775
- it('falls back to enableCellSelection when selectable is omitted', async () => {
776
- const { api, target, destroy } = await mountGrid({ enableCellSelection: false })
777
- try {
778
- await tick()
779
- await clickCell(target, 0)
780
- expect(api.getSelected()).toEqual([])
781
- } finally {
782
- destroy()
783
- }
784
- })
785
-
786
- it('api.setOption("selectable", false) turns it off at runtime', async () => {
787
- const { api, target, destroy } = await mountGrid()
788
- try {
789
- await tick()
790
- await clickCell(target, 0)
791
- expect(api.getSelected().length).toBe(1)
792
-
793
- api.setOption('selectable', false)
794
- await tick()
795
- api.selectCells([])
796
- await tick()
797
- await clickCell(target, 0)
798
- expect(api.getSelected()).toEqual([])
799
- } finally {
800
- destroy()
801
- }
802
- })
803
- })
804
-
805
- /**
806
- * Guards the promise the homepage "A real grid in ~15 lines" section makes:
807
- * the snippet shown there is shortcuts-only - no `features` import, no
808
- * `editorType` - so every capability it advertises must work from exactly
809
- * those props. The section drifted from the component once already (it showed
810
- * a bare grid beside a preview captioned "sort, filter, select, edit ... are
811
- * all live"), which is what this test exists to catch.
812
- */
813
- describe('SvGrid - homepage snippet (shortcuts only, no features import)', () => {
814
- const snippetRows = [
815
- { firstName: 'Ada', age: 36, status: 'active' },
816
- { firstName: 'Linus', age: 54, status: 'active' },
817
- { firstName: 'Grace', age: 85, status: 'inactive' },
818
- ]
819
- const snippetColumns = [
820
- { field: 'firstName', header: 'First name' },
821
- { field: 'age', header: 'Age' },
822
- { field: 'status', header: 'Status' },
823
- ] as unknown as typeof personColumns
824
-
825
- function mountSnippet(): Promise<MountResult> {
826
- return new Promise((resolveApi, rejectApi) => {
827
- const target = document.createElement('div')
828
- document.body.appendChild(target)
829
- const app = mount(SvGrid, {
830
- target,
831
- props: {
832
- data: snippetRows,
833
- columns: snippetColumns,
834
- // The four shortcuts the snippet passes - and nothing else.
835
- sortable: true,
836
- filterable: true,
837
- editable: true,
838
- selectable: true,
839
- getRowId: (r: { firstName: string }) => r.firstName,
840
- rowHeight: 36,
841
- containerHeight: 300,
842
- virtualization: false,
843
- onApiReady(api: SvGridApi<any, any>) {
844
- resolveApi({
845
- api,
846
- target,
847
- destroy: () => {
848
- unmount(app)
849
- target.remove()
850
- },
851
- } as unknown as MountResult)
852
- },
853
- } as never,
854
- })
855
- if (!app) rejectApi(new Error('mount failed'))
856
- })
857
- }
858
-
859
- it('sortable works without importing rowSortingFeature', async () => {
860
- const { api, destroy } = await mountSnippet()
861
- try {
862
- await tick()
863
- api.setSort('age', 'asc')
864
- await tick()
865
- expect(api.getState().sorting).toEqual([{ id: 'age', desc: false }])
866
- } finally {
867
- destroy()
868
- }
869
- })
870
-
871
- it('editable opens an editor on a column with no editorType', async () => {
872
- const { api, target, destroy } = await mountSnippet()
873
- try {
874
- await tick()
875
- api.startEditing(0, 'firstName')
876
- await tick()
877
- expect(target.querySelectorAll('.sv-grid-cell-editing').length).toBe(1)
878
- } finally {
879
- destroy()
880
- }
881
- })
882
-
883
- it('selectable gives cell selection', async () => {
884
- const { api, target, destroy } = await mountSnippet()
885
- try {
886
- await tick()
887
- const cell = target.querySelector('.sv-grid-cell:not(.sv-grid-selection-cell)')
888
- cell?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
889
- await tick()
890
- expect(api.getSelected().length).toBe(1)
891
- } finally {
892
- destroy()
893
- }
894
- })
895
-
896
- it('the checkbox column renders and toggles a row - no feature import needed', async () => {
897
- const { api, target, destroy } = await mountSnippet()
898
- try {
899
- await tick()
900
- // [0] is the header select-all; [1] is the first row.
901
- const boxes = target.querySelectorAll<HTMLElement>('.sv-grid-checkbox')
902
- expect(boxes.length).toBeGreaterThan(1)
903
- boxes[1]?.click()
904
- await tick()
1
+ /**
2
+ * Broad behavioral test suite for the <SvGrid /> render component.
3
+ *
4
+ * The intent is wide coverage on a single file - SvGrid.svelte is ~3000
5
+ * lines and most of its branches are reachable only when the component is
6
+ * mounted with a particular prop combination. Each test below exercises a
7
+ * different feature set so the whole file's effect graph is traversed.
8
+ *
9
+ * Pattern (re-used across files):
10
+ * 1. mount SvGrid in jsdom with the props that trigger the feature
11
+ * 2. wait one microtask for $effects to flush
12
+ * 3. assert on the api object delivered via onApiReady, or on visible DOM
13
+ * 4. unmount
14
+ */
15
+
16
+ import { describe, expect, it } from 'vitest'
17
+ import { mount, unmount } from 'svelte'
18
+ import SvGrid from './SvGrid.svelte'
19
+ import {
20
+ columnFilteringFeature,
21
+ columnGroupingFeature,
22
+ createCoreRowModel,
23
+ createExpandedRowModel,
24
+ createFilteredRowModel,
25
+ createGroupedRowModel,
26
+ createPaginatedRowModel,
27
+ createSortedRowModel,
28
+ rowExpandingFeature,
29
+ rowPaginationFeature,
30
+ rowSelectionFeature,
31
+ rowSortingFeature,
32
+ sortFns,
33
+ tableFeatures,
34
+ } from './index'
35
+ import type { ColumnDef, SvGridApi } from './index'
36
+
37
+ type Person = {
38
+ id: number
39
+ name: string
40
+ team: string
41
+ age: number
42
+ active: boolean
43
+ joined: string
44
+ salary: number
45
+ }
46
+
47
+ const fullFeatures = tableFeatures({
48
+ columnFilteringFeature,
49
+ columnGroupingFeature,
50
+ rowExpandingFeature,
51
+ rowPaginationFeature,
52
+ rowSelectionFeature,
53
+ rowSortingFeature,
54
+ })
55
+
56
+ const people: Person[] = [
57
+ { id: 1, name: 'Ada Lovelace', team: 'Research', age: 38, active: true, joined: '2020-01-10', salary: 142_000 },
58
+ { id: 2, name: 'Grace Hopper', team: 'Compilers', age: 42, active: true, joined: '2018-05-22', salary: 158_000 },
59
+ { id: 3, name: 'Alan Turing', team: 'Research', age: 41, active: false, joined: '2021-09-01', salary: 138_000 },
60
+ { id: 4, name: 'Margaret Hamilton', team: 'Apollo', age: 35, active: true, joined: '2019-03-15', salary: 165_000 },
61
+ { id: 5, name: 'Linus Torvalds', team: 'Kernel', age: 54, active: true, joined: '2017-11-30', salary: 175_000 },
62
+ { id: 6, name: 'Donald Knuth', team: 'Research', age: 86, active: false, joined: '2010-06-06', salary: 120_000 },
63
+ { id: 7, name: 'Brian Kernighan', team: 'Compilers', age: 80, active: true, joined: '2012-02-02', salary: 160_000 },
64
+ { id: 8, name: 'Dennis Ritchie', team: 'Kernel', age: 70, active: false, joined: '2009-07-14', salary: 110_000 },
65
+ ]
66
+
67
+ const personColumns: ColumnDef<typeof fullFeatures, Person>[] = [
68
+ { field: 'name', header: 'Name', width: 200 },
69
+ { field: 'team', header: 'Team', width: 160 },
70
+ {
71
+ field: 'age',
72
+ header: 'Age',
73
+ width: 100,
74
+ editorType: 'number',
75
+ format: { type: 'number' },
76
+ },
77
+ {
78
+ field: 'active',
79
+ header: 'Active',
80
+ width: 100,
81
+ editorType: 'checkbox',
82
+ },
83
+ {
84
+ field: 'joined',
85
+ header: 'Joined',
86
+ width: 140,
87
+ editorType: 'date',
88
+ format: { type: 'number' },
89
+ },
90
+ {
91
+ field: 'salary',
92
+ header: 'Salary',
93
+ width: 140,
94
+ editorType: 'number',
95
+ format: { type: 'currency', currency: 'USD' },
96
+ },
97
+ ]
98
+
99
+ type MountResult = {
100
+ api: SvGridApi<typeof fullFeatures, Person>
101
+ target: HTMLElement
102
+ destroy: () => void
103
+ }
104
+
105
+ function mountGrid(overrides: Record<string, unknown> = {}): Promise<MountResult> {
106
+ return new Promise((resolveApi, rejectApi) => {
107
+ const target = document.createElement('div')
108
+ target.style.width = '1200px'
109
+ target.style.height = '600px'
110
+ document.body.appendChild(target)
111
+
112
+ let capturedApi: SvGridApi<typeof fullFeatures, Person> | null = null
113
+
114
+ const app = mount(SvGrid, {
115
+ target,
116
+ props: {
117
+ data: people,
118
+ columns: personColumns,
119
+ features: fullFeatures,
120
+ _rowModels: {
121
+ coreRowModel: createCoreRowModel(),
122
+ filteredRowModel: createFilteredRowModel(),
123
+ sortedRowModel: createSortedRowModel(sortFns),
124
+ groupedRowModel: createGroupedRowModel(),
125
+ expandedRowModel: createExpandedRowModel(),
126
+ paginatedRowModel: createPaginatedRowModel(),
127
+ },
128
+ rowHeight: 36,
129
+ containerHeight: 480,
130
+ virtualization: false,
131
+ onApiReady(api: SvGridApi<typeof fullFeatures, Person>) {
132
+ capturedApi = api
133
+ resolveApi({
134
+ api,
135
+ target,
136
+ destroy: () => {
137
+ unmount(app)
138
+ target.remove()
139
+ },
140
+ })
141
+ },
142
+ ...overrides,
143
+ } as any,
144
+ })
145
+
146
+ queueMicrotask(() => {
147
+ if (!capturedApi) rejectApi(new Error('onApiReady never fired'))
148
+ })
149
+ })
150
+ }
151
+
152
+ const tick = () => Promise.resolve()
153
+
154
+ describe('SvGrid - core render', () => {
155
+ it('mounts and renders the column headers', async () => {
156
+ const { target, destroy } = await mountGrid()
157
+ try {
158
+ const headers = target.querySelectorAll('[data-svgrid-header-col]')
159
+ // jsdom reports 0 for offsetWidth so column virtualization may render
160
+ // a subset. The point of this assertion is just that the header row
161
+ // exists and is populated.
162
+ expect(headers.length).toBeGreaterThan(0)
163
+ } finally {
164
+ destroy()
165
+ }
166
+ })
167
+
168
+ it('renders the rows in the body', async () => {
169
+ const { target, destroy } = await mountGrid()
170
+ try {
171
+ await tick()
172
+ const rows = target.querySelectorAll('[data-svgrid-row]')
173
+ // Pagination defaults to off in our mount - all rows render.
174
+ expect(rows.length).toBeGreaterThanOrEqual(people.length)
175
+ } finally {
176
+ destroy()
177
+ }
178
+ })
179
+
180
+ it('renders the empty message when data is empty', async () => {
181
+ const { target, destroy } = await mountGrid({ data: [], emptyMessage: 'No rows yet' })
182
+ try {
183
+ await tick()
184
+ expect(target.textContent).toContain('No rows yet')
185
+ } finally {
186
+ destroy()
187
+ }
188
+ })
189
+
190
+ it('renders a loading state when `loading` is true', async () => {
191
+ const { target, destroy } = await mountGrid({ loading: true })
192
+ try {
193
+ await tick()
194
+ const statusEls = target.querySelectorAll('[role="status"]')
195
+ expect(statusEls.length).toBeGreaterThan(0)
196
+ } finally {
197
+ destroy()
198
+ }
199
+ })
200
+
201
+ it('renders an error state when `error` is set', async () => {
202
+ const { target, destroy } = await mountGrid({ error: 'Something broke' })
203
+ try {
204
+ await tick()
205
+ expect(target.textContent).toContain('Something broke')
206
+ } finally {
207
+ destroy()
208
+ }
209
+ })
210
+ })
211
+
212
+ describe('SvGrid - sort', () => {
213
+ it('setSort then clearSort updates filters via api', async () => {
214
+ const { api, destroy } = await mountGrid()
215
+ try {
216
+ api.setSort('name', 'asc')
217
+ await tick()
218
+ api.setSort('name', 'desc')
219
+ await tick()
220
+ api.setSort('name', null)
221
+ await tick()
222
+ api.clearSort()
223
+ await tick()
224
+ // No throw + state consistent = pass.
225
+ expect(api.getData().length).toBe(people.length)
226
+ } finally {
227
+ destroy()
228
+ }
229
+ })
230
+ })
231
+
232
+ describe('SvGrid - pagination', () => {
233
+ it('shows fewer displayed rows than total when paginated', async () => {
234
+ const { api, destroy } = await mountGrid({ showPagination: true, pageSize: 3 })
235
+ try {
236
+ await tick()
237
+ const displayed = api.getDisplayedRows()
238
+ expect(displayed.length).toBeLessThanOrEqual(3)
239
+ } finally {
240
+ destroy()
241
+ }
242
+ })
243
+ })
244
+
245
+ describe('SvGrid - global filter', () => {
246
+ it('showGlobalFilter renders the input', async () => {
247
+ const { target, destroy } = await mountGrid({ showGlobalFilter: true })
248
+ try {
249
+ await tick()
250
+ const input = target.querySelector(
251
+ '.sv-grid-global-filter input, input[type="text"]',
252
+ )
253
+ expect(input).not.toBeNull()
254
+ } finally {
255
+ destroy()
256
+ }
257
+ })
258
+ })
259
+
260
+ describe('SvGrid - column filter row', () => {
261
+ it('showColumnFilters renders the filter row inputs', async () => {
262
+ const { target, destroy } = await mountGrid({ showColumnFilters: true })
263
+ try {
264
+ await tick()
265
+ const inputs = target.querySelectorAll('.sv-grid-column-filter')
266
+ // One filter input per filterable column.
267
+ expect(inputs.length).toBeGreaterThan(0)
268
+ } finally {
269
+ destroy()
270
+ }
271
+ })
272
+ })
273
+
274
+ describe('SvGrid - inline editing', () => {
275
+ it('enables enableInlineEditing without throwing', async () => {
276
+ const { api, destroy } = await mountGrid({ enableInlineEditing: true })
277
+ try {
278
+ // setCellValue should mutate internal data.
279
+ api.setCellValue(0, 'name', 'Renamed Person')
280
+ await tick()
281
+ const data = api.getData()
282
+ expect((data[0] as Person).name).toBe('Renamed Person')
283
+ } finally {
284
+ destroy()
285
+ }
286
+ })
287
+ })
288
+
289
+ describe('SvGrid - row selection', () => {
290
+ it('showRowSelection renders the checkbox column', async () => {
291
+ const { target, destroy } = await mountGrid({ showRowSelection: true })
292
+ try {
293
+ await tick()
294
+ const checkboxes = target.querySelectorAll('.sv-grid-checkbox')
295
+ // At least the header + one per row.
296
+ expect(checkboxes.length).toBeGreaterThan(0)
297
+ } finally {
298
+ destroy()
299
+ }
300
+ })
301
+ })
302
+
303
+ describe('SvGrid - capability shortcuts', () => {
304
+ it('sortable + filterable enable sorting/filtering UI with no features prop', async () => {
305
+ const { target, destroy } = await mountGrid({
306
+ features: undefined,
307
+ sortable: true,
308
+ filterable: true,
309
+ })
310
+ try {
311
+ await tick()
312
+ // rowSortingFeature injected -> headers expose aria-sort.
313
+ expect(
314
+ target.querySelectorAll('[role="columnheader"][aria-sort]').length,
315
+ ).toBeGreaterThan(0)
316
+ // columnFilteringFeature injected -> per-column filter button renders.
317
+ expect(target.querySelector('.sv-grid-col-filter-btn')).toBeTruthy()
318
+ } finally {
319
+ destroy()
320
+ }
321
+ })
322
+
323
+ it('sortable={false} overrides a rowSortingFeature passed via features', async () => {
324
+ // The harness default `fullFeatures` includes rowSortingFeature.
325
+ const { target, destroy } = await mountGrid({ sortable: false })
326
+ try {
327
+ await tick()
328
+ expect(
329
+ target.querySelector('[role="columnheader"][aria-sort]'),
330
+ ).toBeNull()
331
+ } finally {
332
+ destroy()
333
+ }
334
+ })
335
+
336
+ it('pageable toggles the pagination footer', async () => {
337
+ const off = await mountGrid({ pageable: false })
338
+ try {
339
+ await tick()
340
+ expect(off.target.querySelector('.sv-grid-pagination')).toBeNull()
341
+ } finally {
342
+ off.destroy()
343
+ }
344
+ const on = await mountGrid({ features: undefined, pageable: true })
345
+ try {
346
+ await tick()
347
+ expect(on.target.querySelector('.sv-grid-pagination')).toBeTruthy()
348
+ } finally {
349
+ on.destroy()
350
+ }
351
+ })
352
+
353
+ it('paginationPosition mounts + gates the bottom pager', async () => {
354
+ // The top pager is gated on measurement (jsdom does not measure), so it's not
355
+ // asserted here; the visible top pager is covered by the browser/e2e check.
356
+ // This guards the mount path (a top footer rendered pre-measure used to crash).
357
+ for (const paginationPosition of ['top', 'both', 'bottom'] as const) {
358
+ const g = await mountGrid({ showPagination: true, paginationPosition })
359
+ try {
360
+ await tick()
361
+ expect(g.api).toBeTruthy()
362
+ const bottoms = [...g.target.querySelectorAll('.sv-grid-pagination')].filter((el) => !el.classList.contains('sv-grid-pagination-top'))
363
+ expect(bottoms.length).toBe(paginationPosition === 'top' ? 0 : 1) // no bottom pager when position is 'top'
364
+ } finally {
365
+ g.destroy()
366
+ }
367
+ }
368
+ })
369
+
370
+ it('pageSizeOptions is accepted by the pager', async () => {
371
+ const g = await mountGrid({ showPagination: true, pageSizeOptions: [5, 15, 45] })
372
+ try {
373
+ await tick()
374
+ expect(g.target.querySelector('.sv-grid-pagination')).toBeTruthy()
375
+ } finally {
376
+ g.destroy()
377
+ }
378
+ })
379
+
380
+ it('editable={false} blocks inline editing entry points', async () => {
381
+ const { api, target, destroy } = await mountGrid({ editable: false })
382
+ try {
383
+ await tick()
384
+ // Double-clicking a body cell must not open an editor when editing
385
+ // is disabled via the shortcut.
386
+ const cell = target.querySelector<HTMLElement>('.sv-grid-cell')
387
+ cell?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
388
+ await tick()
389
+ expect(target.querySelector('.sv-grid-cell-editor')).toBeNull()
390
+ expect(api.getData().length).toBe(people.length)
391
+ } finally {
392
+ destroy()
393
+ }
394
+ })
395
+ })
396
+
397
+ describe('SvGrid - cell selection', () => {
398
+ it('enableCellSelection mounts without error', async () => {
399
+ const { api, destroy } = await mountGrid({ enableCellSelection: true })
400
+ try {
401
+ await tick()
402
+ expect(api.getData().length).toBe(people.length)
403
+ } finally {
404
+ destroy()
405
+ }
406
+ })
407
+ })
408
+
409
+ describe('SvGrid - grouping + aggregation', () => {
410
+ it('setGroupBy mounts + the codepath runs without throwing', async () => {
411
+ const { api, target, destroy } = await mountGrid({
412
+ enableRowSummaries: true,
413
+ showGroupingControls: true,
414
+ })
415
+ try {
416
+ api.setGroupBy(['team'])
417
+ await tick()
418
+ await tick()
419
+ // Sanity: the grid is still mounted and the API responsive.
420
+ const data = api.getData()
421
+ expect(data.length).toBe(people.length)
422
+ // Group rendering depends on row model + container dimensions in jsdom;
423
+ // just verify the codepath does not throw.
424
+ expect(target).toBeTruthy()
425
+ } finally {
426
+ destroy()
427
+ }
428
+ })
429
+ })
430
+
431
+ describe('SvGrid - column add / remove / visibility', () => {
432
+ it('addColumn and removeColumn round-trip', async () => {
433
+ const { api, destroy } = await mountGrid()
434
+ try {
435
+ api.addColumn({ id: 'id-col', field: 'id', header: 'ID', width: 60 } as any, 'left')
436
+ await tick()
437
+ api.addColumn(
438
+ { id: 'extra-tag', field: 'team', header: 'Extra', width: 60 } as any,
439
+ 'right',
440
+ )
441
+ await tick()
442
+ api.addColumn(
443
+ { id: 'mid-col', field: 'name', header: 'Mid', width: 60 } as any,
444
+ 1,
445
+ )
446
+ await tick()
447
+ api.removeColumn('id-col')
448
+ await tick()
449
+ // No throw means the codepath was exercised.
450
+ expect(typeof api.getData).toBe('function')
451
+ } finally {
452
+ destroy()
453
+ }
454
+ })
455
+
456
+ it('setColumnVisible toggles visibility flag', async () => {
457
+ const { api, destroy } = await mountGrid()
458
+ try {
459
+ expect(api.isColumnVisible('name')).toBe(true)
460
+ api.setColumnVisible('name', false)
461
+ await tick()
462
+ expect(api.isColumnVisible('name')).toBe(false)
463
+ api.setColumnVisible('name', true)
464
+ await tick()
465
+ expect(api.isColumnVisible('name')).toBe(true)
466
+ } finally {
467
+ destroy()
468
+ }
469
+ })
470
+
471
+ it('columns marked `visible: false` start hidden but stay listed', async () => {
472
+ const columns: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map(
473
+ (c) => (c.field === 'team' ? { ...c, visible: false } : c),
474
+ )
475
+ const { api, destroy } = await mountGrid({ columns })
476
+ try {
477
+ // Starts hidden...
478
+ expect(api.isColumnVisible('team')).toBe(false)
479
+ // ...but is still listed in getColumns() (so a Choose Columns UI
480
+ // can offer it), just with visible=false.
481
+ const team = api.getColumns().find((c) => c.id === 'team')
482
+ expect(team).toBeDefined()
483
+ expect(team?.visible).toBe(false)
484
+ // A later user toggle wins over the initial flag.
485
+ api.setColumnVisible('team', true)
486
+ await tick()
487
+ expect(api.isColumnVisible('team')).toBe(true)
488
+ } finally {
489
+ destroy()
490
+ }
491
+ })
492
+ })
493
+
494
+ describe('SvGrid - row add / remove', () => {
495
+ it('addRow at top / bottom / specific index', async () => {
496
+ const { api, destroy } = await mountGrid()
497
+ try {
498
+ const before = api.getData().length
499
+ api.addRow({ id: 99, name: 'Top', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'top')
500
+ api.addRow({ id: 100, name: 'Bottom', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 'bottom')
501
+ api.addRow({ id: 101, name: 'Mid', team: 'X', age: 20, active: true, joined: '2025', salary: 1 }, 2)
502
+ await tick()
503
+ expect(api.getData().length).toBe(before + 3)
504
+ } finally {
505
+ destroy()
506
+ }
507
+ })
508
+
509
+ it('addRows / removeRow / removeRows', async () => {
510
+ const { api, destroy } = await mountGrid()
511
+ try {
512
+ const before = api.getData().length
513
+ api.addRows(
514
+ [
515
+ { id: 90, name: 'A', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
516
+ { id: 91, name: 'B', team: 'X', age: 1, active: true, joined: '2025', salary: 1 },
517
+ ],
518
+ 'bottom',
519
+ )
520
+ await tick()
521
+ api.removeRow(0)
522
+ await tick()
523
+ api.removeRows([0, 1])
524
+ await tick()
525
+ expect(api.getData().length).toBe(before + 2 - 3)
526
+ } finally {
527
+ destroy()
528
+ }
529
+ })
530
+ })
531
+
532
+ describe('SvGrid - getCellValue', () => {
533
+ it('returns the cell value for a known row+column', async () => {
534
+ const { api, destroy } = await mountGrid()
535
+ try {
536
+ const value = api.getCellValue(0, 'name')
537
+ expect(value).toBe(people[0]!.name)
538
+ } finally {
539
+ destroy()
540
+ }
541
+ })
542
+
543
+ it('returns undefined for unknown row or column', async () => {
544
+ const { api, destroy } = await mountGrid()
545
+ try {
546
+ expect(api.getCellValue(999, 'name')).toBeUndefined()
547
+ expect(api.getCellValue(0, 'no-such-col')).toBeUndefined()
548
+ } finally {
549
+ destroy()
550
+ }
551
+ })
552
+ })
553
+
554
+ describe('SvGrid - row number column', () => {
555
+ it('showRowNumbers renders the leading row-number column', async () => {
556
+ const { target, destroy } = await mountGrid({ showRowNumbers: true })
557
+ try {
558
+ await tick()
559
+ const rnCells = target.querySelectorAll('.sv-grid-row-number-cell')
560
+ expect(rnCells.length).toBeGreaterThan(0)
561
+ } finally {
562
+ destroy()
563
+ }
564
+ })
565
+ })
566
+
567
+ describe('SvGrid - external sort / filter', () => {
568
+ it('externalSort + onSortingChange fires without re-ordering rows', async () => {
569
+ let captured: Array<{ id: string; desc: boolean }> | null = null
570
+ const { api, destroy } = await mountGrid({
571
+ externalSort: true,
572
+ onSortingChange: (s: any) => (captured = s),
573
+ })
574
+ try {
575
+ api.setSort('age', 'desc')
576
+ await tick()
577
+ expect(captured).not.toBeNull()
578
+ } finally {
579
+ destroy()
580
+ }
581
+ })
582
+
583
+ it('externalFilter + onFiltersChange fires when a filter is set', async () => {
584
+ let calls = 0
585
+ const { api, destroy } = await mountGrid({
586
+ externalFilter: true,
587
+ onFiltersChange: () => {
588
+ calls += 1
589
+ },
590
+ })
591
+ try {
592
+ api.setFilter('team', { operator: 'contains', value: 'Research' })
593
+ await tick()
594
+ expect(calls).toBeGreaterThan(0)
595
+ } finally {
596
+ destroy()
597
+ }
598
+ })
599
+ })
600
+
601
+ describe('SvGrid - custom snippet cell', () => {
602
+ it('accepts a custom cell renderer without throwing', async () => {
603
+ // Build a snippet via raw object - the SvGrid component reads
604
+ // `column.columnDef.cell` and pipes it through FlexRender. Returning a
605
+ // plain string is the simplest renderer; tests only need to verify the
606
+ // render path doesn't crash.
607
+ const customCols: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map((c) =>
608
+ c.field === 'name'
609
+ ? {
610
+ ...c,
611
+ cell: (ctx: any) => `★ ${(ctx.row.original as Person).name}`,
612
+ }
613
+ : c,
614
+ )
615
+ const { target, destroy } = await mountGrid({ columns: customCols })
616
+ try {
617
+ await tick()
618
+ expect(target.textContent).toContain('★')
619
+ } finally {
620
+ destroy()
621
+ }
622
+ })
623
+ })
624
+
625
+ describe('SvGrid - filterMode shortcuts', () => {
626
+ it('filterMode="row" + global filter input wiring', async () => {
627
+ const { destroy } = await mountGrid({ filterMode: 'row' })
628
+ try {
629
+ await tick()
630
+ // No assertion needed - exercising the codepath is enough.
631
+ expect(true).toBe(true)
632
+ } finally {
633
+ destroy()
634
+ }
635
+ })
636
+ it('filterMode="global"', async () => {
637
+ const { destroy } = await mountGrid({ filterMode: 'global' })
638
+ try {
639
+ await tick()
640
+ expect(true).toBe(true)
641
+ } finally {
642
+ destroy()
643
+ }
644
+ })
645
+ it('filterMode="none"', async () => {
646
+ const { destroy } = await mountGrid({ filterMode: 'none' })
647
+ try {
648
+ await tick()
649
+ expect(true).toBe(true)
650
+ } finally {
651
+ destroy()
652
+ }
653
+ })
654
+ })
655
+
656
+ describe('SvGrid - selectionMode shortcuts', () => {
657
+ it('selectionMode="row"', async () => {
658
+ const { destroy } = await mountGrid({ selectionMode: 'row' })
659
+ try {
660
+ await tick()
661
+ expect(true).toBe(true)
662
+ } finally {
663
+ destroy()
664
+ }
665
+ })
666
+ it('selectionMode="cell"', async () => {
667
+ const { destroy } = await mountGrid({ selectionMode: 'cell' })
668
+ try {
669
+ await tick()
670
+ expect(true).toBe(true)
671
+ } finally {
672
+ destroy()
673
+ }
674
+ })
675
+ it('selectionMode="both"', async () => {
676
+ const { destroy } = await mountGrid({ selectionMode: 'both' })
677
+ try {
678
+ await tick()
679
+ expect(true).toBe(true)
680
+ } finally {
681
+ destroy()
682
+ }
683
+ })
684
+ it('selectionMode="none"', async () => {
685
+ const { destroy } = await mountGrid({ selectionMode: 'none' })
686
+ try {
687
+ await tick()
688
+ expect(true).toBe(true)
689
+ } finally {
690
+ destroy()
691
+ }
692
+ })
693
+ })
694
+
695
+ describe('SvGrid - fitColumns', () => {
696
+ it('mounts with fitColumns=true without throwing', async () => {
697
+ const { destroy } = await mountGrid({ fitColumns: true })
698
+ try {
699
+ await tick()
700
+ expect(true).toBe(true)
701
+ } finally {
702
+ destroy()
703
+ }
704
+ })
705
+ })
706
+
707
+ describe('SvGrid - selectable shortcut (cell selection)', () => {
708
+ // The gated path a user actually takes: pointerdown on a cell runs through
709
+ // createSelection.setSelection, which bails on !enableCellSelectionEffective.
710
+ // Asserting through it (rather than api.selectCells, which writes the range
711
+ // directly and bypasses the gate) is what makes these tests meaningful.
712
+ // Deliberately skips `.sv-grid-selection-cell`: the checkbox column is a
713
+ // different surface and a pointerdown there starts no range, so clicking it
714
+ // would make every assertion below pass vacuously.
715
+ async function clickCell(target: HTMLElement, index: number) {
716
+ const cells = target.querySelectorAll(
717
+ '.sv-grid-cell:not(.sv-grid-selection-cell)',
718
+ )
719
+ const cell = cells[index]
720
+ if (!cell) throw new Error(`no data cell at index ${index} (found ${cells.length})`)
721
+ cell.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
722
+ await tick()
723
+ }
724
+
725
+ it('is ON by default - selectionMode defaults to "both"', async () => {
726
+ const { api, target, destroy } = await mountGrid()
727
+ try {
728
+ await tick()
729
+ await clickCell(target, 0)
730
+ expect(api.getSelected().length).toBe(1)
731
+ } finally {
732
+ destroy()
733
+ }
734
+ })
735
+
736
+ it('selectable={false} turns cell selection off', async () => {
737
+ const { api, target, destroy } = await mountGrid({ selectable: false })
738
+ try {
739
+ await tick()
740
+ await clickCell(target, 0)
741
+ expect(api.getSelected()).toEqual([])
742
+ } finally {
743
+ destroy()
744
+ }
745
+ })
746
+
747
+ it('selectable={true} wins over selectionMode="none"', async () => {
748
+ const { api, target, destroy } = await mountGrid({
749
+ selectable: true,
750
+ selectionMode: 'none',
751
+ })
752
+ try {
753
+ await tick()
754
+ await clickCell(target, 0)
755
+ expect(api.getSelected().length).toBe(1)
756
+ } finally {
757
+ destroy()
758
+ }
759
+ })
760
+
761
+ it('selectable={false} wins over enableCellSelection={true}', async () => {
762
+ const { api, target, destroy } = await mountGrid({
763
+ selectable: false,
764
+ enableCellSelection: true,
765
+ })
766
+ try {
767
+ await tick()
768
+ await clickCell(target, 0)
769
+ expect(api.getSelected()).toEqual([])
770
+ } finally {
771
+ destroy()
772
+ }
773
+ })
774
+
775
+ it('falls back to enableCellSelection when selectable is omitted', async () => {
776
+ const { api, target, destroy } = await mountGrid({ enableCellSelection: false })
777
+ try {
778
+ await tick()
779
+ await clickCell(target, 0)
780
+ expect(api.getSelected()).toEqual([])
781
+ } finally {
782
+ destroy()
783
+ }
784
+ })
785
+
786
+ it('api.setOption("selectable", false) turns it off at runtime', async () => {
787
+ const { api, target, destroy } = await mountGrid()
788
+ try {
789
+ await tick()
790
+ await clickCell(target, 0)
791
+ expect(api.getSelected().length).toBe(1)
792
+
793
+ api.setOption('selectable', false)
794
+ await tick()
795
+ api.selectCells([])
796
+ await tick()
797
+ await clickCell(target, 0)
798
+ expect(api.getSelected()).toEqual([])
799
+ } finally {
800
+ destroy()
801
+ }
802
+ })
803
+ })
804
+
805
+ /**
806
+ * Guards the promise the homepage "A real grid in ~15 lines" section makes:
807
+ * the snippet shown there is shortcuts-only - no `features` import, no
808
+ * `editorType` - so every capability it advertises must work from exactly
809
+ * those props. The section drifted from the component once already (it showed
810
+ * a bare grid beside a preview captioned "sort, filter, select, edit ... are
811
+ * all live"), which is what this test exists to catch.
812
+ */
813
+ describe('SvGrid - homepage snippet (shortcuts only, no features import)', () => {
814
+ const snippetRows = [
815
+ { firstName: 'Ada', age: 36, status: 'active' },
816
+ { firstName: 'Linus', age: 54, status: 'active' },
817
+ { firstName: 'Grace', age: 85, status: 'inactive' },
818
+ ]
819
+ const snippetColumns = [
820
+ { field: 'firstName', header: 'First name' },
821
+ { field: 'age', header: 'Age' },
822
+ { field: 'status', header: 'Status' },
823
+ ] as unknown as typeof personColumns
824
+
825
+ function mountSnippet(): Promise<MountResult> {
826
+ return new Promise((resolveApi, rejectApi) => {
827
+ const target = document.createElement('div')
828
+ document.body.appendChild(target)
829
+ const app = mount(SvGrid, {
830
+ target,
831
+ props: {
832
+ data: snippetRows,
833
+ columns: snippetColumns,
834
+ // The four shortcuts the snippet passes - and nothing else.
835
+ sortable: true,
836
+ filterable: true,
837
+ editable: true,
838
+ selectable: true,
839
+ getRowId: (r: { firstName: string }) => r.firstName,
840
+ rowHeight: 36,
841
+ containerHeight: 300,
842
+ virtualization: false,
843
+ onApiReady(api: SvGridApi<any, any>) {
844
+ resolveApi({
845
+ api,
846
+ target,
847
+ destroy: () => {
848
+ unmount(app)
849
+ target.remove()
850
+ },
851
+ } as unknown as MountResult)
852
+ },
853
+ } as never,
854
+ })
855
+ if (!app) rejectApi(new Error('mount failed'))
856
+ })
857
+ }
858
+
859
+ it('sortable works without importing rowSortingFeature', async () => {
860
+ const { api, destroy } = await mountSnippet()
861
+ try {
862
+ await tick()
863
+ api.setSort('age', 'asc')
864
+ await tick()
865
+ expect(api.getState().sorting).toEqual([{ id: 'age', desc: false }])
866
+ } finally {
867
+ destroy()
868
+ }
869
+ })
870
+
871
+ it('editable opens an editor on a column with no editorType', async () => {
872
+ const { api, target, destroy } = await mountSnippet()
873
+ try {
874
+ await tick()
875
+ api.startEditing(0, 'firstName')
876
+ await tick()
877
+ expect(target.querySelectorAll('.sv-grid-cell-editing').length).toBe(1)
878
+ } finally {
879
+ destroy()
880
+ }
881
+ })
882
+
883
+ it('selectable gives cell selection', async () => {
884
+ const { api, target, destroy } = await mountSnippet()
885
+ try {
886
+ await tick()
887
+ const cell = target.querySelector('.sv-grid-cell:not(.sv-grid-selection-cell)')
888
+ cell?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
889
+ await tick()
890
+ expect(api.getSelected().length).toBe(1)
891
+ } finally {
892
+ destroy()
893
+ }
894
+ })
895
+
896
+ it('the checkbox column renders and toggles a row - no feature import needed', async () => {
897
+ const { api, target, destroy } = await mountSnippet()
898
+ try {
899
+ await tick()
900
+ // [0] is the header select-all; [1] is the first row.
901
+ const boxes = target.querySelectorAll<HTMLElement>('.sv-grid-checkbox')
902
+ expect(boxes.length).toBeGreaterThan(1)
903
+ boxes[1]?.click()
904
+ await tick()
905
905
  const names = (api.getSelectedRows() as unknown as { firstName: string }[]).map(
906
906
  (r) => r.firstName,
907
907
  )
908
- expect(names).toEqual(['Ada'])
909
- } finally {
910
- destroy()
911
- }
912
- })
913
- })
908
+ expect(names).toEqual(['Ada'])
909
+ } finally {
910
+ destroy()
911
+ }
912
+ })
913
+ })