@svgrid/grid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +39 -0
  3. package/dist/FlexRender.svelte +96 -0
  4. package/dist/FlexRender.svelte.d.ts +49 -0
  5. package/dist/SvGrid.svelte +8742 -0
  6. package/dist/SvGrid.svelte.d.ts +381 -0
  7. package/dist/SvGridChart.svelte +653 -0
  8. package/dist/SvGridChart.svelte.d.ts +25 -0
  9. package/dist/SvGridChart.test.d.ts +1 -0
  10. package/dist/SvGridChart.test.js +62 -0
  11. package/dist/SvGridDropdown.svelte +675 -0
  12. package/dist/SvGridDropdown.svelte.d.ts +40 -0
  13. package/dist/a11y.contract.test.d.ts +1 -0
  14. package/dist/a11y.contract.test.js +39 -0
  15. package/dist/a11y.d.ts +38 -0
  16. package/dist/a11y.js +37 -0
  17. package/dist/a11y.test.d.ts +1 -0
  18. package/dist/a11y.test.js +44 -0
  19. package/dist/cell-formatting.d.ts +17 -0
  20. package/dist/cell-formatting.js +141 -0
  21. package/dist/cell-formatting.test.d.ts +1 -0
  22. package/dist/cell-formatting.test.js +234 -0
  23. package/dist/chart-export.d.ts +14 -0
  24. package/dist/chart-export.js +112 -0
  25. package/dist/chart.d.ts +226 -0
  26. package/dist/chart.js +732 -0
  27. package/dist/chart.test.d.ts +1 -0
  28. package/dist/chart.test.js +289 -0
  29. package/dist/collaboration.d.ts +74 -0
  30. package/dist/collaboration.js +98 -0
  31. package/dist/collaboration.test.d.ts +1 -0
  32. package/dist/collaboration.test.js +65 -0
  33. package/dist/conditional-formatting.d.ts +91 -0
  34. package/dist/conditional-formatting.js +170 -0
  35. package/dist/conditional-formatting.test.d.ts +1 -0
  36. package/dist/conditional-formatting.test.js +87 -0
  37. package/dist/core.coverage.test.d.ts +1 -0
  38. package/dist/core.coverage.test.js +186 -0
  39. package/dist/core.d.ts +370 -0
  40. package/dist/core.js +568 -0
  41. package/dist/core.performance.test.d.ts +1 -0
  42. package/dist/core.performance.test.js +29 -0
  43. package/dist/createGrid.svelte.d.ts +6 -0
  44. package/dist/createGrid.svelte.js +17 -0
  45. package/dist/createGrid.test.d.ts +1 -0
  46. package/dist/createGrid.test.js +9 -0
  47. package/dist/createGridState.svelte.d.ts +3 -0
  48. package/dist/createGridState.svelte.js +13 -0
  49. package/dist/editors/cell-editors.d.ts +23 -0
  50. package/dist/editors/cell-editors.js +97 -0
  51. package/dist/editors/cell-editors.test.d.ts +1 -0
  52. package/dist/editors/cell-editors.test.js +75 -0
  53. package/dist/fill-patterns.d.ts +30 -0
  54. package/dist/fill-patterns.js +207 -0
  55. package/dist/fill-patterns.test.d.ts +1 -0
  56. package/dist/fill-patterns.test.js +81 -0
  57. package/dist/filtering/excel-filters.d.ts +26 -0
  58. package/dist/filtering/excel-filters.js +64 -0
  59. package/dist/filtering/excel-filters.test.d.ts +1 -0
  60. package/dist/filtering/excel-filters.test.js +116 -0
  61. package/dist/filtering/locale-filter.test.d.ts +13 -0
  62. package/dist/filtering/locale-filter.test.js +189 -0
  63. package/dist/flex-render.d.ts +1 -0
  64. package/dist/flex-render.js +2 -0
  65. package/dist/flex-render.test.d.ts +1 -0
  66. package/dist/flex-render.test.js +142 -0
  67. package/dist/group-aggregate.test.d.ts +1 -0
  68. package/dist/group-aggregate.test.js +32 -0
  69. package/dist/index.d.ts +29 -0
  70. package/dist/index.js +27 -0
  71. package/dist/keyboard.d.ts +8 -0
  72. package/dist/keyboard.js +91 -0
  73. package/dist/keyboard.test.d.ts +1 -0
  74. package/dist/keyboard.test.js +27 -0
  75. package/dist/merge-objects.d.ts +9 -0
  76. package/dist/merge-objects.js +28 -0
  77. package/dist/named-views.d.ts +42 -0
  78. package/dist/named-views.js +80 -0
  79. package/dist/named-views.test.d.ts +1 -0
  80. package/dist/named-views.test.js +57 -0
  81. package/dist/new-features.test.d.ts +1 -0
  82. package/dist/new-features.test.js +183 -0
  83. package/dist/render-component.d.ts +13 -0
  84. package/dist/render-component.js +14 -0
  85. package/dist/render-component.test.d.ts +1 -0
  86. package/dist/render-component.test.js +41 -0
  87. package/dist/server-data-source.d.ts +76 -0
  88. package/dist/server-data-source.js +83 -0
  89. package/dist/server-data-source.test.d.ts +1 -0
  90. package/dist/server-data-source.test.js +81 -0
  91. package/dist/sparkline.d.ts +56 -0
  92. package/dist/sparkline.js +97 -0
  93. package/dist/sparkline.test.d.ts +1 -0
  94. package/dist/sparkline.test.js +52 -0
  95. package/dist/static-functions.d.ts +1 -0
  96. package/dist/static-functions.js +1 -0
  97. package/dist/subscribe.d.ts +9 -0
  98. package/dist/subscribe.js +31 -0
  99. package/dist/subscribe.test.d.ts +1 -0
  100. package/dist/subscribe.test.js +93 -0
  101. package/dist/sv-grid-scrollbar.d.ts +1 -0
  102. package/dist/sv-grid-scrollbar.js +310 -0
  103. package/dist/svgrid-wrapper.types.d.ts +341 -0
  104. package/dist/svgrid-wrapper.types.js +1 -0
  105. package/dist/svgrid.api-extensions.test.d.ts +14 -0
  106. package/dist/svgrid.api-extensions.test.js +334 -0
  107. package/dist/svgrid.api.test.d.ts +15 -0
  108. package/dist/svgrid.api.test.js +169 -0
  109. package/dist/svgrid.behavior.test.d.ts +15 -0
  110. package/dist/svgrid.behavior.test.js +605 -0
  111. package/dist/svgrid.column-reorder.test.d.ts +15 -0
  112. package/dist/svgrid.column-reorder.test.js +224 -0
  113. package/dist/svgrid.features.test.d.ts +1 -0
  114. package/dist/svgrid.features.test.js +129 -0
  115. package/dist/svgrid.interaction.test.d.ts +9 -0
  116. package/dist/svgrid.interaction.test.js +318 -0
  117. package/dist/svgrid.locale-filtering.test.d.ts +15 -0
  118. package/dist/svgrid.locale-filtering.test.js +261 -0
  119. package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
  120. package/dist/svgrid.new-features.wrapper.test.js +136 -0
  121. package/dist/svgrid.row-pinning.test.d.ts +14 -0
  122. package/dist/svgrid.row-pinning.test.js +250 -0
  123. package/dist/svgrid.set-filter.test.d.ts +17 -0
  124. package/dist/svgrid.set-filter.test.js +268 -0
  125. package/dist/svgrid.wrapper.test.d.ts +1 -0
  126. package/dist/svgrid.wrapper.test.js +33 -0
  127. package/dist/test-setup.d.ts +1 -0
  128. package/dist/test-setup.js +29 -0
  129. package/dist/transaction.test.d.ts +1 -0
  130. package/dist/transaction.test.js +89 -0
  131. package/dist/virtualization/column-virtualizer.d.ts +20 -0
  132. package/dist/virtualization/column-virtualizer.js +19 -0
  133. package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
  134. package/dist/virtualization/column-virtualizer.test.js +25 -0
  135. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
  136. package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
  137. package/dist/virtualization/types.d.ts +28 -0
  138. package/dist/virtualization/types.js +1 -0
  139. package/dist/virtualization/virtualizer.d.ts +13 -0
  140. package/dist/virtualization/virtualizer.js +232 -0
  141. package/dist/virtualization/virtualizer.test.d.ts +1 -0
  142. package/dist/virtualization/virtualizer.test.js +41 -0
  143. package/package.json +69 -0
  144. package/src/FlexRender.svelte +96 -0
  145. package/src/SvGrid.svelte +8742 -0
  146. package/src/SvGridChart.svelte +653 -0
  147. package/src/SvGridChart.test.ts +69 -0
  148. package/src/SvGridDropdown.svelte +675 -0
  149. package/src/a11y.contract.test.ts +49 -0
  150. package/src/a11y.test.ts +59 -0
  151. package/src/a11y.ts +59 -0
  152. package/src/cell-formatting.test.ts +273 -0
  153. package/src/cell-formatting.ts +169 -0
  154. package/src/chart-export.ts +144 -0
  155. package/src/chart.test.ts +311 -0
  156. package/src/chart.ts +934 -0
  157. package/src/collaboration.test.ts +74 -0
  158. package/src/collaboration.ts +161 -0
  159. package/src/conditional-formatting.test.ts +107 -0
  160. package/src/conditional-formatting.ts +239 -0
  161. package/src/core.coverage.test.ts +230 -0
  162. package/src/core.performance.test.ts +30 -0
  163. package/src/core.ts +991 -0
  164. package/src/createGrid.svelte.ts +42 -0
  165. package/src/createGrid.test.ts +10 -0
  166. package/src/createGridState.svelte.ts +17 -0
  167. package/src/editors/cell-editors.test.ts +80 -0
  168. package/src/editors/cell-editors.ts +127 -0
  169. package/src/fill-patterns.test.ts +93 -0
  170. package/src/fill-patterns.ts +229 -0
  171. package/src/filtering/excel-filters.test.ts +131 -0
  172. package/src/filtering/excel-filters.ts +101 -0
  173. package/src/filtering/locale-filter.test.ts +220 -0
  174. package/src/flex-render.test.ts +143 -0
  175. package/src/flex-render.ts +3 -0
  176. package/src/group-aggregate.test.ts +48 -0
  177. package/src/index.ts +159 -0
  178. package/src/keyboard.test.ts +59 -0
  179. package/src/keyboard.ts +97 -0
  180. package/src/merge-objects.ts +48 -0
  181. package/src/named-views.test.ts +66 -0
  182. package/src/named-views.ts +120 -0
  183. package/src/new-features.test.ts +217 -0
  184. package/src/render-component.test.ts +51 -0
  185. package/src/render-component.ts +28 -0
  186. package/src/server-data-source.test.ts +88 -0
  187. package/src/server-data-source.ts +163 -0
  188. package/src/sparkline.test.ts +59 -0
  189. package/src/sparkline.ts +164 -0
  190. package/src/static-functions.ts +11 -0
  191. package/src/subscribe.test.ts +103 -0
  192. package/src/subscribe.ts +38 -0
  193. package/src/sv-grid-scrollbar.ts +347 -0
  194. package/src/svgrid-wrapper.types.ts +382 -0
  195. package/src/svgrid.api-extensions.test.ts +362 -0
  196. package/src/svgrid.api.test.ts +192 -0
  197. package/src/svgrid.behavior.test.ts +657 -0
  198. package/src/svgrid.column-reorder.test.ts +234 -0
  199. package/src/svgrid.features.test.ts +157 -0
  200. package/src/svgrid.interaction.test.ts +355 -0
  201. package/src/svgrid.locale-filtering.test.ts +259 -0
  202. package/src/svgrid.new-features.wrapper.test.ts +164 -0
  203. package/src/svgrid.row-pinning.test.ts +266 -0
  204. package/src/svgrid.set-filter.test.ts +270 -0
  205. package/src/svgrid.wrapper.test.ts +35 -0
  206. package/src/test-setup.ts +37 -0
  207. package/src/transaction.test.ts +100 -0
  208. package/src/virtualization/column-virtualizer.test.ts +27 -0
  209. package/src/virtualization/column-virtualizer.ts +30 -0
  210. package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
  211. package/src/virtualization/types.ts +30 -0
  212. package/src/virtualization/virtualizer.test.ts +47 -0
  213. package/src/virtualization/virtualizer.ts +270 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Integration tests for the `filterLocale` prop. Mounts a real
3
+ * <SvGrid> with accented data, drives the column-menu filter via
4
+ * `api.setFilter()`, then asserts the visible row count via
5
+ * `api.getDisplayedRows()`.
6
+ *
7
+ * Covers:
8
+ * - Accent-insensitive `contains` operator on the menu filter
9
+ * - Diacritic-stripped global filter (via `setGlobalFilter` not in
10
+ * the public api yet; here we use `api.setFilter('city', ...)`
11
+ * which exercises the same pipeline)
12
+ * - filterLocale prop pass-through (es-ES, de-DE)
13
+ * - Regression: ASCII queries still narrow rows
14
+ */
15
+ export {};
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Integration tests for the `filterLocale` prop. Mounts a real
3
+ * <SvGrid> with accented data, drives the column-menu filter via
4
+ * `api.setFilter()`, then asserts the visible row count via
5
+ * `api.getDisplayedRows()`.
6
+ *
7
+ * Covers:
8
+ * - Accent-insensitive `contains` operator on the menu filter
9
+ * - Diacritic-stripped global filter (via `setGlobalFilter` not in
10
+ * the public api yet; here we use `api.setFilter('city', ...)`
11
+ * which exercises the same pipeline)
12
+ * - filterLocale prop pass-through (es-ES, de-DE)
13
+ * - Regression: ASCII queries still narrow rows
14
+ */
15
+ import { describe, expect, it } from 'vitest';
16
+ import { mount, unmount } from 'svelte';
17
+ import SvGrid from './SvGrid.svelte';
18
+ import { columnFilteringFeature, createCoreRowModel, createFilteredRowModel, createSortedRowModel, rowSortingFeature, sortFns, tableFeatures, } from './index';
19
+ const features = tableFeatures({ columnFilteringFeature, rowSortingFeature });
20
+ const rows = [
21
+ { id: 1, city: 'München', country: 'Deutschland', region: 'Europe', population: 1510000 },
22
+ { id: 2, city: 'Köln', country: 'Deutschland', region: 'Europe', population: 1080000 },
23
+ { id: 3, city: 'Genève', country: 'Suisse', region: 'Europe', population: 203000 },
24
+ { id: 4, city: 'Montréal', country: 'Canada', region: 'Americas', population: 1780000 },
25
+ { id: 5, city: 'México', country: 'México', region: 'Americas', population: 9210000 },
26
+ { id: 6, city: 'São Paulo', country: 'Brasil', region: 'Americas', population: 12330000 },
27
+ { id: 7, city: 'Tōkyō', country: '日本', region: 'Asia', population: 13960000 },
28
+ { id: 8, city: 'İstanbul', country: 'Türkiye', region: 'Asia', population: 15840000 },
29
+ { id: 9, city: 'Málaga', country: 'España', region: 'Europe', population: 580000 },
30
+ { id: 10, city: 'Berlin', country: 'Deutschland', region: 'Europe', population: 3770000 },
31
+ ];
32
+ const cols = [
33
+ { field: 'city', header: 'City', width: 180, editable: false },
34
+ { field: 'country', header: 'Country', width: 160, editable: false },
35
+ { field: 'region', header: 'Region', width: 110, editable: false },
36
+ { field: 'population', header: 'Population', width: 130, editable: false, align: 'right' },
37
+ ];
38
+ function mountGrid(opts = {}) {
39
+ return new Promise((res, rej) => {
40
+ const target = document.createElement('div');
41
+ document.body.appendChild(target);
42
+ let api = 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
+ },
54
+ containerHeight: 360,
55
+ virtualization: false,
56
+ columnVirtualization: false,
57
+ showPagination: false,
58
+ showColumnFilters: false,
59
+ showGlobalFilter: false,
60
+ showRowSelection: false,
61
+ filterLocale: opts.filterLocale,
62
+ onApiReady(received) {
63
+ api = received;
64
+ res({
65
+ api,
66
+ destroy: () => { unmount(app); target.remove(); },
67
+ });
68
+ },
69
+ },
70
+ });
71
+ queueMicrotask(() => { if (!api)
72
+ rej(new Error('onApiReady never fired')); });
73
+ });
74
+ }
75
+ const tick = () => new Promise((r) => queueMicrotask(r));
76
+ describe('SvGrid filter - default locale (no prop)', () => {
77
+ it('"munch" matches "München" via contains operator', async () => {
78
+ const { api, destroy } = await mountGrid();
79
+ try {
80
+ api.setFilter('city', { operator: 'contains', value: 'munch' });
81
+ await tick();
82
+ const visible = api.getDisplayedRows();
83
+ expect(visible.map((r) => r.city)).toEqual(['München']);
84
+ }
85
+ finally {
86
+ destroy();
87
+ }
88
+ });
89
+ it('"geneve" matches "Genève"', async () => {
90
+ const { api, destroy } = await mountGrid();
91
+ try {
92
+ api.setFilter('city', { operator: 'contains', value: 'geneve' });
93
+ await tick();
94
+ const visible = api.getDisplayedRows();
95
+ expect(visible.map((r) => r.city)).toEqual(['Genève']);
96
+ }
97
+ finally {
98
+ destroy();
99
+ }
100
+ });
101
+ it('"sao paulo" matches "São Paulo"', async () => {
102
+ const { api, destroy } = await mountGrid();
103
+ try {
104
+ api.setFilter('city', { operator: 'contains', value: 'sao paulo' });
105
+ await tick();
106
+ const visible = api.getDisplayedRows();
107
+ expect(visible.map((r) => r.city)).toEqual(['São Paulo']);
108
+ }
109
+ finally {
110
+ destroy();
111
+ }
112
+ });
113
+ it('"tokyo" matches "Tōkyō"', async () => {
114
+ const { api, destroy } = await mountGrid();
115
+ try {
116
+ api.setFilter('city', { operator: 'contains', value: 'tokyo' });
117
+ await tick();
118
+ const visible = api.getDisplayedRows();
119
+ expect(visible.map((r) => r.city)).toEqual(['Tōkyō']);
120
+ }
121
+ finally {
122
+ destroy();
123
+ }
124
+ });
125
+ it('"istanbul" matches "İstanbul" (dotted-I default folding)', async () => {
126
+ const { api, destroy } = await mountGrid();
127
+ try {
128
+ api.setFilter('city', { operator: 'contains', value: 'istanbul' });
129
+ await tick();
130
+ const visible = api.getDisplayedRows();
131
+ expect(visible.map((r) => r.city)).toEqual(['İstanbul']);
132
+ }
133
+ finally {
134
+ destroy();
135
+ }
136
+ });
137
+ it('"deutschland" matches all German cities', async () => {
138
+ const { api, destroy } = await mountGrid();
139
+ try {
140
+ api.setFilter('country', { operator: 'contains', value: 'deutschland' });
141
+ await tick();
142
+ const visible = api.getDisplayedRows();
143
+ expect(visible.map((r) => r.city).sort()).toEqual(['Berlin', 'Köln', 'München']);
144
+ }
145
+ finally {
146
+ destroy();
147
+ }
148
+ });
149
+ });
150
+ describe('SvGrid filter - filterLocale="de-DE"', () => {
151
+ it('explicit German locale - "munchen" still matches "München"', async () => {
152
+ const { api, destroy } = await mountGrid({ filterLocale: 'de-DE' });
153
+ try {
154
+ api.setFilter('city', { operator: 'contains', value: 'munchen' });
155
+ await tick();
156
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['München']);
157
+ }
158
+ finally {
159
+ destroy();
160
+ }
161
+ });
162
+ it('"koln" matches "Köln"', async () => {
163
+ const { api, destroy } = await mountGrid({ filterLocale: 'de-DE' });
164
+ try {
165
+ api.setFilter('city', { operator: 'contains', value: 'koln' });
166
+ await tick();
167
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['Köln']);
168
+ }
169
+ finally {
170
+ destroy();
171
+ }
172
+ });
173
+ });
174
+ describe('SvGrid filter - filterLocale="es-ES"', () => {
175
+ it('"mexico" matches "México"', async () => {
176
+ const { api, destroy } = await mountGrid({ filterLocale: 'es-ES' });
177
+ try {
178
+ api.setFilter('city', { operator: 'contains', value: 'mexico' });
179
+ await tick();
180
+ const cities = api.getDisplayedRows().map((r) => r.city);
181
+ expect(cities).toContain('México');
182
+ }
183
+ finally {
184
+ destroy();
185
+ }
186
+ });
187
+ it('"malaga" matches "Málaga"', async () => {
188
+ const { api, destroy } = await mountGrid({ filterLocale: 'es-ES' });
189
+ try {
190
+ api.setFilter('city', { operator: 'contains', value: 'malaga' });
191
+ await tick();
192
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['Málaga']);
193
+ }
194
+ finally {
195
+ destroy();
196
+ }
197
+ });
198
+ });
199
+ describe('SvGrid filter - startsWith + equals operators', () => {
200
+ it('startsWith "mun" matches München', async () => {
201
+ const { api, destroy } = await mountGrid();
202
+ try {
203
+ api.setFilter('city', { operator: 'startsWith', value: 'mun' });
204
+ await tick();
205
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['München']);
206
+ }
207
+ finally {
208
+ destroy();
209
+ }
210
+ });
211
+ it('equals "berlin" matches exactly "Berlin" (case-insensitive)', async () => {
212
+ const { api, destroy } = await mountGrid();
213
+ try {
214
+ api.setFilter('city', { operator: 'equals', value: 'berlin' });
215
+ await tick();
216
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['Berlin']);
217
+ }
218
+ finally {
219
+ destroy();
220
+ }
221
+ });
222
+ it('equals "kolN" matches "Köln" (case + accent insensitive)', async () => {
223
+ const { api, destroy } = await mountGrid();
224
+ try {
225
+ api.setFilter('city', { operator: 'equals', value: 'kolN' });
226
+ await tick();
227
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['Köln']);
228
+ }
229
+ finally {
230
+ destroy();
231
+ }
232
+ });
233
+ });
234
+ describe('SvGrid filter - clear + compose', () => {
235
+ it('clearFilter removes the narrowing', async () => {
236
+ const { api, destroy } = await mountGrid();
237
+ try {
238
+ api.setFilter('city', { operator: 'contains', value: 'munch' });
239
+ await tick();
240
+ expect(api.getDisplayedRows().length).toBe(1);
241
+ api.clearFilter('city');
242
+ await tick();
243
+ expect(api.getDisplayedRows().length).toBe(rows.length);
244
+ }
245
+ finally {
246
+ destroy();
247
+ }
248
+ });
249
+ it('two column filters compose (AND across columns)', async () => {
250
+ const { api, destroy } = await mountGrid();
251
+ try {
252
+ api.setFilter('country', { operator: 'contains', value: 'deutschland' });
253
+ api.setFilter('city', { operator: 'contains', value: 'mun' });
254
+ await tick();
255
+ expect(api.getDisplayedRows().map((r) => r.city)).toEqual(['München']);
256
+ }
257
+ finally {
258
+ destroy();
259
+ }
260
+ });
261
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Source-level smoke tests for the wrapper additions: `getRowId` prop,
3
+ * `cellClass` / `rowClass` rendering, the column-layout API surface
4
+ * (`setColumnWidth` / `getColumnWidths` / `setColumnPinning` /
5
+ * `getColumnPinning`), the `between` operator UI, and the multi-cell
6
+ * paste / cut keyboard plumbing.
7
+ *
8
+ * These are deliberately source-string assertions, not jsdom mounts -
9
+ * Svelte 5 mount inside vitest is brittle (the existing
10
+ * `svgrid.wrapper.test.ts` follows the same pattern). The behaviour
11
+ * itself is covered by integration probes in the gallery (see the
12
+ * pivot expansion + paste/cut probes that were run during build) and
13
+ * the upstream unit tests of the headless engine + Pro pivot.
14
+ */
15
+ import { readFileSync } from 'node:fs';
16
+ import { resolve } from 'node:path';
17
+ import { describe, expect, it } from 'vitest';
18
+ const sourcePath = resolve(__dirname, './SvGrid.svelte');
19
+ const source = readFileSync(sourcePath, 'utf-8');
20
+ const typesPath = resolve(__dirname, './svgrid-wrapper.types.ts');
21
+ const types = readFileSync(typesPath, 'utf-8');
22
+ describe('SvGrid wrapper - getRowId prop', () => {
23
+ it('declares the getRowId prop on the Props type', () => {
24
+ expect(source).toMatch(/getRowId\?:\s*\(row:\s*TData,\s*index:\s*number\)\s*=>\s*string/);
25
+ });
26
+ it('forwards the prop into createSvGrid options', () => {
27
+ expect(source).toMatch(/get getRowId\(\)[\s\S]*?return props\.getRowId/);
28
+ });
29
+ });
30
+ describe('SvGrid wrapper - cellClass + rowClass', () => {
31
+ it('declares the rowClass prop on the Props type', () => {
32
+ expect(source).toMatch(/rowClass\?:\s*\(ctx:\s*\{/);
33
+ });
34
+ it('exposes a resolveClassList helper that normalises array / record / string forms', () => {
35
+ expect(source).toMatch(/function resolveClassList\(/);
36
+ expect(source).toMatch(/Array\.isArray\(value\)/);
37
+ });
38
+ it('computes the row-level class once per render row and threads it onto the <tr>', () => {
39
+ expect(source).toMatch(/function computeRowClass/);
40
+ expect(source).toMatch(/userRowClass = computeRowClass\(row, rowIndex\)/);
41
+ expect(source).toMatch(/class=\{`sv-grid-row \$\{userRowClass\}`\}/);
42
+ });
43
+ it('computes the cell-level class per column and threads it onto the <td>', () => {
44
+ expect(source).toMatch(/function computeCellClass/);
45
+ expect(source).toMatch(/userCellClass = computeCellClass\(row, rendered\.column\)/);
46
+ expect(source).toMatch(/class=\{`sv-grid-cell \$\{userCellClass\}`\}/);
47
+ });
48
+ });
49
+ describe('SvGrid wrapper - column-layout API', () => {
50
+ it('declares setColumnWidth + getColumnWidths in the public type', () => {
51
+ expect(types).toContain('setColumnWidth(columnId: string, width: number): void');
52
+ expect(types).toContain('getColumnWidths(): Record<string, number>');
53
+ });
54
+ it('declares setColumnPinning + getColumnPinning in the public type', () => {
55
+ expect(types).toMatch(/setColumnPinning\(pinning:\s*\{/);
56
+ expect(types).toContain('getColumnPinning(): { left: string[]; right: string[] }');
57
+ });
58
+ it('implements all four methods in the api object', () => {
59
+ expect(source).toMatch(/setColumnWidth\(columnId: string, width: number\)/);
60
+ expect(source).toMatch(/getColumnWidths\(\)/);
61
+ expect(source).toMatch(/setColumnPinning\(pinning\)/);
62
+ expect(source).toMatch(/getColumnPinning\(\)/);
63
+ });
64
+ it('clamps setColumnWidth to MIN_COLUMN_WIDTH', () => {
65
+ // Confirm the clamp is in the code path so a caller can't drop a
66
+ // column below the minimum by passing 0 or a negative number.
67
+ expect(source).toMatch(/Math\.max\(MIN_COLUMN_WIDTH/);
68
+ });
69
+ it('defensive-copies setColumnPinning inputs so callers cannot mutate state', () => {
70
+ expect(source).toMatch(/Array\.from\(new Set\(pinning\.left/);
71
+ expect(source).toMatch(/Array\.from\(new Set\(pinning\.right/);
72
+ });
73
+ });
74
+ describe('SvGrid wrapper - between filter operator', () => {
75
+ it("includes 'between' in the FilterOperator type", () => {
76
+ expect(source).toMatch(/"between"/);
77
+ });
78
+ it("includes 'between' in NUMBER_OPERATORS + DATE_OPERATORS but NOT in TEXT_OPERATORS", () => {
79
+ // The list literals appear in order, so we slice each block out
80
+ // separately and check membership.
81
+ const numOps = source.match(/const NUMBER_OPERATORS[^]*?\];/)?.[0] ?? '';
82
+ expect(numOps).toContain('"between"');
83
+ const dateOps = source.match(/const DATE_OPERATORS[^]*?\];/)?.[0] ?? '';
84
+ expect(dateOps).toContain('"between"');
85
+ const textOps = source.match(/const TEXT_OPERATORS[^]*?\];/)?.[0] ?? '';
86
+ expect(textOps).not.toContain('"between"');
87
+ });
88
+ it('renders a second input ("To") when the operator is between', () => {
89
+ expect(source).toMatch(/menuActiveOperator === "between"/);
90
+ expect(source).toMatch(/placeholder="To"/);
91
+ });
92
+ it('treats the filter as inactive until BOTH endpoints are non-empty', () => {
93
+ // The active-filters filter() should require value AND valueTo.
94
+ expect(source).toMatch(/if \(f\.operator === "between"\) \{[^}]*f\.value\.trim\(\)\.length > 0[^}]*f\.valueTo[^}]*\.trim\(\)\.length > 0/);
95
+ });
96
+ it('forwards valueTo through applyExcelFilter', () => {
97
+ expect(source).toMatch(/valueTo: filter\.operator === "between" \? filter\.valueTo : undefined/);
98
+ });
99
+ it('publishes valueTo in the onFiltersChange payload only for between clauses', () => {
100
+ expect(source).toMatch(/f\.operator === "between" && f\.valueTo\s*\?\s*\{ valueTo: f\.valueTo \}/);
101
+ });
102
+ it('exposes valueTo on the imperative setFilter input + getFilters output', () => {
103
+ expect(types).toMatch(/setFilter\(\s*columnId: string,[\s\S]*?valueTo\?: string/);
104
+ expect(types).toMatch(/getFilters\(\)[\s\S]*?valueTo\?: string/);
105
+ });
106
+ });
107
+ describe('SvGrid wrapper - multi-cell paste + cut + delete', () => {
108
+ it('fills the selection range when the clipboard payload is 1x1', () => {
109
+ expect(source).toMatch(/clipboardIsSingleCell\s*=\s*\n?\s*lines\.length === 1 && \(lines\[0\]\?\.split\("\\t"\)\.length \?\? 0\) === 1/);
110
+ expect(source).toMatch(/selectionIsRange = startRow !== endRow \|\| startCol !== endCol/);
111
+ expect(source).toMatch(/fillRange = clipboardIsSingleCell && selectionIsRange/);
112
+ });
113
+ it('walks the entire selection rect when filling', () => {
114
+ expect(source).toMatch(/const rowSpan = fillRange \? endRow - startRow \+ 1 : lines\.length/);
115
+ expect(source).toMatch(/const colSpan = fillRange/);
116
+ });
117
+ it('implements clearSelectedCells() that touches every editable cell in the range', () => {
118
+ expect(source).toMatch(/function clearSelectedCells\(\): boolean/);
119
+ expect(source).toMatch(/parseEditorValue\(editorType, ""\)/);
120
+ });
121
+ it('wires Ctrl/Cmd+X to cutSelectionToClipboard', () => {
122
+ expect(source).toMatch(/lower === "x"/);
123
+ expect(source).toMatch(/void cutSelectionToClipboard\(\)/);
124
+ expect(source).toMatch(/async function cutSelectionToClipboard/);
125
+ // Confirm both calls live inside the cutSelectionToClipboard body
126
+ // (copy first, then clear). A comment between them is fine.
127
+ const body = source.match(/async function cutSelectionToClipboard\(\)[^{]*\{([\s\S]*?)^\s\s\}/m)?.[1] ?? '';
128
+ expect(body).toContain('copySelectionToClipboard();');
129
+ expect(body).toContain('clearSelectedCells();');
130
+ expect(body.indexOf('copySelectionToClipboard()')).toBeLessThan(body.indexOf('clearSelectedCells()'));
131
+ });
132
+ it('wires Delete + Backspace to clearSelectedCells (without clipboard interaction)', () => {
133
+ expect(source).toMatch(/event\.key === "Delete" \|\| event\.key === "Backspace"/);
134
+ expect(source).toMatch(/if \(clearSelectedCells\(\)\)\s*\{/);
135
+ });
136
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Component tests for the engine-level row pinning props
3
+ * (`pinnedTopRows` / `pinnedBottomRows`). Mount a real <SvGrid>,
4
+ * pass the props, then assert on the rendered DOM:
5
+ *
6
+ * - pinned-top / pinned-bottom <tbody>s are rendered
7
+ * - each pinned row carries the expected class + data attributes
8
+ * - column widths + cell values match the schema
9
+ * - read-only invariants hold (no editor cell, no checkbox cell)
10
+ * - reactive prop changes re-render
11
+ * - stacking attributes (data-pinned-index) are present
12
+ * - CSS variables for sticky offsets are set on the shell
13
+ */
14
+ export {};
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Component tests for the engine-level row pinning props
3
+ * (`pinnedTopRows` / `pinnedBottomRows`). Mount a real <SvGrid>,
4
+ * pass the props, then assert on the rendered DOM:
5
+ *
6
+ * - pinned-top / pinned-bottom <tbody>s are rendered
7
+ * - each pinned row carries the expected class + data attributes
8
+ * - column widths + cell values match the schema
9
+ * - read-only invariants hold (no editor cell, no checkbox cell)
10
+ * - reactive prop changes re-render
11
+ * - stacking attributes (data-pinned-index) are present
12
+ * - CSS variables for sticky offsets are set on the shell
13
+ */
14
+ import { describe, expect, it } from 'vitest';
15
+ import { mount, unmount, flushSync } from 'svelte';
16
+ import SvGrid from './SvGrid.svelte';
17
+ import { columnFilteringFeature, createCoreRowModel, createFilteredRowModel, createSortedRowModel, rowSortingFeature, sortFns, tableFeatures, } from './index';
18
+ const features = tableFeatures({ columnFilteringFeature, rowSortingFeature });
19
+ const rows = [
20
+ { id: 'ACC-1', account: 'Helios', arr: 120000, seats: 40 },
21
+ { id: 'ACC-2', account: 'Vertex', arr: 85000, seats: 22 },
22
+ { id: 'ACC-3', account: 'Atlas', arr: 250000, seats: 80 },
23
+ { id: 'ACC-4', account: 'Quantum', arr: 60000, seats: 15 },
24
+ ];
25
+ const cols = [
26
+ { field: 'id', header: 'ID', width: 100, editable: false },
27
+ { field: 'account', header: 'Account', width: 200, editable: false },
28
+ { field: 'arr', header: 'ARR', width: 130, editable: false, align: 'right',
29
+ format: { type: 'number', options: { style: 'currency', currency: 'USD', maximumFractionDigits: 0 } } },
30
+ { field: 'seats', header: 'Seats', width: 90, editable: false, align: 'right' },
31
+ ];
32
+ function mountGrid(opts = {}) {
33
+ return new Promise((res, rej) => {
34
+ const target = document.createElement('div');
35
+ document.body.appendChild(target);
36
+ const propsState = { ...opts };
37
+ let api = null;
38
+ const app = mount(SvGrid, {
39
+ target,
40
+ props: {
41
+ data: rows,
42
+ columns: cols,
43
+ features,
44
+ _rowModels: {
45
+ coreRowModel: createCoreRowModel(),
46
+ filteredRowModel: createFilteredRowModel(),
47
+ sortedRowModel: createSortedRowModel(sortFns),
48
+ },
49
+ containerHeight: 480,
50
+ virtualization: false,
51
+ columnVirtualization: false,
52
+ enableInlineEditing: false,
53
+ showPagination: false,
54
+ showColumnFilters: false,
55
+ showGlobalFilter: false,
56
+ showRowSelection: false,
57
+ pinnedTopRows: propsState.pinnedTopRows,
58
+ pinnedBottomRows: propsState.pinnedBottomRows,
59
+ onApiReady(received) {
60
+ api = received;
61
+ res({
62
+ target,
63
+ api,
64
+ destroy: () => {
65
+ unmount(app);
66
+ target.remove();
67
+ },
68
+ setProps(next) {
69
+ propsState.pinnedTopRows = next.pinnedTopRows;
70
+ propsState.pinnedBottomRows = next.pinnedBottomRows;
71
+ app
72
+ .pinnedTopRows = next.pinnedTopRows;
73
+ app
74
+ .pinnedBottomRows = next.pinnedBottomRows;
75
+ flushSync();
76
+ },
77
+ });
78
+ },
79
+ },
80
+ });
81
+ queueMicrotask(() => { if (!api)
82
+ rej(new Error('onApiReady never fired')); });
83
+ });
84
+ }
85
+ const tick = () => new Promise((r) => queueMicrotask(r));
86
+ const TOTALS = {
87
+ id: '⌃ TOTALS', account: 'All accounts',
88
+ arr: rows.reduce((s, r) => s + r.arr, 0),
89
+ seats: rows.reduce((s, r) => s + r.seats, 0),
90
+ };
91
+ const BENCHMARK = {
92
+ id: '⌃ BENCH', account: 'Industry benchmark',
93
+ arr: 180000, seats: 95,
94
+ };
95
+ const PAGE = {
96
+ id: '⌄ PAGE', account: 'Visible page',
97
+ arr: rows.reduce((s, r) => s + r.arr, 0),
98
+ seats: rows.reduce((s, r) => s + r.seats, 0),
99
+ };
100
+ describe('SvGrid - pinnedTopRows render', () => {
101
+ it('renders a pinned-top tbody when prop is provided', async () => {
102
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] });
103
+ try {
104
+ await tick();
105
+ const topBody = target.querySelector('tbody.sv-grid-pinned-top-body');
106
+ expect(topBody, 'pinned-top tbody should exist').not.toBeNull();
107
+ const trs = topBody.querySelectorAll('tr.sv-grid-pinned-row-top');
108
+ expect(trs.length).toBe(1);
109
+ }
110
+ finally {
111
+ destroy();
112
+ }
113
+ });
114
+ it('does NOT render the tbody when prop is empty / undefined', async () => {
115
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [] });
116
+ try {
117
+ await tick();
118
+ const topBody = target.querySelector('tbody.sv-grid-pinned-top-body');
119
+ expect(topBody).toBeNull();
120
+ }
121
+ finally {
122
+ destroy();
123
+ }
124
+ });
125
+ it('renders the row values using the column schema (format applied)', async () => {
126
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] });
127
+ try {
128
+ await tick();
129
+ const tds = target
130
+ .querySelectorAll('tr.sv-grid-pinned-row-top td');
131
+ expect(tds.length).toBe(cols.length);
132
+ // ARR cell should be currency-formatted ($515,000) - currency
133
+ // symbol exact form may vary by ICU build; assert a "$" plus the
134
+ // raw number with grouping is present.
135
+ const arrCellText = (tds[2]?.textContent ?? '').trim();
136
+ expect(arrCellText).toMatch(/\$/);
137
+ expect(arrCellText).toContain('515');
138
+ }
139
+ finally {
140
+ destroy();
141
+ }
142
+ });
143
+ it('stacks multiple pinned-top rows with data-pinned-index', async () => {
144
+ const { target, destroy } = await mountGrid({
145
+ pinnedTopRows: [TOTALS, BENCHMARK],
146
+ });
147
+ try {
148
+ await tick();
149
+ const trs = target.querySelectorAll('tr.sv-grid-pinned-row-top');
150
+ expect(trs.length).toBe(2);
151
+ expect(trs[0].getAttribute('data-pinned-row')).toBe('top');
152
+ expect(trs[0].getAttribute('data-pinned-index')).toBe('0');
153
+ expect(trs[1].getAttribute('data-pinned-index')).toBe('1');
154
+ }
155
+ finally {
156
+ destroy();
157
+ }
158
+ });
159
+ it('marks pinned cells with the data-pinned-row attribute', async () => {
160
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] });
161
+ try {
162
+ await tick();
163
+ const tr = target.querySelector('tr.sv-grid-pinned-row-top');
164
+ expect(tr.getAttribute('data-pinned-row')).toBe('top');
165
+ }
166
+ finally {
167
+ destroy();
168
+ }
169
+ });
170
+ });
171
+ describe('SvGrid - pinnedBottomRows render', () => {
172
+ it('renders a pinned-bottom tbody when prop is provided', async () => {
173
+ const { target, destroy } = await mountGrid({ pinnedBottomRows: [PAGE] });
174
+ try {
175
+ await tick();
176
+ const bottomBody = target.querySelector('tbody.sv-grid-pinned-bottom-body');
177
+ expect(bottomBody).not.toBeNull();
178
+ const trs = bottomBody.querySelectorAll('tr.sv-grid-pinned-row-bottom');
179
+ expect(trs.length).toBe(1);
180
+ expect(trs[0].getAttribute('data-pinned-row')).toBe('bottom');
181
+ }
182
+ finally {
183
+ destroy();
184
+ }
185
+ });
186
+ it('renders bottom rows AFTER the regular tbody', async () => {
187
+ const { target, destroy } = await mountGrid({ pinnedBottomRows: [PAGE] });
188
+ try {
189
+ await tick();
190
+ const bodies = target.querySelectorAll('tbody');
191
+ // [regular .sv-grid-body, .sv-grid-pinned-bottom-body]
192
+ // In the DOM order, the bottom tbody must come after the main body.
193
+ const classes = Array.from(bodies).map((b) => b.className);
194
+ const mainIdx = classes.findIndex((c) => c.includes('sv-grid-body') && !c.includes('pinned'));
195
+ const botIdx = classes.findIndex((c) => c.includes('sv-grid-pinned-bottom-body'));
196
+ expect(mainIdx).toBeGreaterThanOrEqual(0);
197
+ expect(botIdx).toBeGreaterThan(mainIdx);
198
+ }
199
+ finally {
200
+ destroy();
201
+ }
202
+ });
203
+ });
204
+ describe('SvGrid - pinned rows are read-only', () => {
205
+ it('pinned-row cells do not carry the editing class even with inline editing enabled', async () => {
206
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] });
207
+ try {
208
+ await tick();
209
+ const editingCells = target.querySelectorAll('tr.sv-grid-pinned-row-top td.sv-grid-cell-editing');
210
+ expect(editingCells.length).toBe(0);
211
+ }
212
+ finally {
213
+ destroy();
214
+ }
215
+ });
216
+ it('pinned rows do not carry the selection checkbox cell', async () => {
217
+ const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] });
218
+ try {
219
+ await tick();
220
+ // The selection cell class is `.sv-grid-selection-cell`. It may
221
+ // appear in the regular row when row-selection is on, but never
222
+ // with a working <button role="checkbox"> inside it on a pinned
223
+ // row (the snippet renders an empty <td>).
224
+ const checkboxes = target.querySelectorAll('tr.sv-grid-pinned-row-top button[role="checkbox"]');
225
+ expect(checkboxes.length).toBe(0);
226
+ }
227
+ finally {
228
+ destroy();
229
+ }
230
+ });
231
+ });
232
+ describe('SvGrid - pinned rows + CSS variables', () => {
233
+ it('sets --sg-thead-h and --sg-pinned-row-h CSS variables on the shell', async () => {
234
+ const { target, destroy } = await mountGrid({
235
+ pinnedTopRows: [TOTALS],
236
+ pinnedBottomRows: [PAGE],
237
+ });
238
+ try {
239
+ await tick();
240
+ const shell = target.querySelector('.sv-grid-shell');
241
+ expect(shell, 'shell should exist').not.toBeNull();
242
+ const style = shell.getAttribute('style') ?? '';
243
+ expect(style).toContain('--sg-thead-h');
244
+ expect(style).toContain('--sg-pinned-row-h');
245
+ }
246
+ finally {
247
+ destroy();
248
+ }
249
+ });
250
+ });