argent-grid 0.1.0 → 0.2.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 (108) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +2 -2
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/screenshots.spec.ts +52 -0
  28. package/e2e/theming.spec.ts +35 -0
  29. package/e2e/visual.spec.ts +91 -0
  30. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  31. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  32. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  33. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  37. package/package.json +20 -6
  38. package/plan.md +50 -18
  39. package/playwright.config.ts +38 -0
  40. package/setup-vitest.ts +10 -13
  41. package/src/lib/argent-grid.module.ts +10 -12
  42. package/src/lib/components/argent-grid.component.css +327 -76
  43. package/src/lib/components/argent-grid.component.html +186 -64
  44. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  45. package/src/lib/components/argent-grid.component.ts +642 -189
  46. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  47. package/src/lib/components/set-filter/set-filter.component.ts +302 -0
  48. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  49. package/src/lib/directives/click-outside.directive.ts +19 -0
  50. package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
  51. package/src/lib/rendering/canvas-renderer.ts +418 -305
  52. package/src/lib/rendering/live-data-handler.ts +110 -0
  53. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  54. package/src/lib/rendering/render/blit.spec.ts +16 -27
  55. package/src/lib/rendering/render/blit.ts +48 -36
  56. package/src/lib/rendering/render/cells.spec.ts +132 -0
  57. package/src/lib/rendering/render/cells.ts +46 -24
  58. package/src/lib/rendering/render/column-utils.ts +73 -0
  59. package/src/lib/rendering/render/hit-test.ts +55 -0
  60. package/src/lib/rendering/render/index.ts +79 -76
  61. package/src/lib/rendering/render/lines.ts +43 -43
  62. package/src/lib/rendering/render/primitives.ts +161 -0
  63. package/src/lib/rendering/render/theme.spec.ts +8 -12
  64. package/src/lib/rendering/render/theme.ts +7 -10
  65. package/src/lib/rendering/render/types.ts +2 -2
  66. package/src/lib/rendering/render/walk.spec.ts +35 -38
  67. package/src/lib/rendering/render/walk.ts +60 -50
  68. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  69. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  70. package/src/lib/rendering/utils/index.ts +1 -1
  71. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  72. package/src/lib/services/grid.service.spec.ts +1165 -201
  73. package/src/lib/services/grid.service.ts +819 -187
  74. package/src/lib/themes/parts/color-schemes.ts +132 -0
  75. package/src/lib/themes/parts/icon-sets.ts +258 -0
  76. package/src/lib/themes/theme-builder.ts +347 -0
  77. package/src/lib/themes/theme-quartz.ts +72 -0
  78. package/src/lib/themes/types.ts +238 -0
  79. package/src/lib/types/ag-grid-types.ts +73 -14
  80. package/src/public-api.ts +39 -9
  81. package/src/stories/Advanced.stories.ts +188 -0
  82. package/src/stories/ArgentGrid.stories.ts +277 -0
  83. package/src/stories/Benchmark.stories.ts +74 -0
  84. package/src/stories/CellRenderers.stories.ts +221 -0
  85. package/src/stories/Filtering.stories.ts +252 -0
  86. package/src/stories/Grouping.stories.ts +217 -0
  87. package/src/stories/Theming.stories.ts +124 -0
  88. package/src/stories/benchmark-wrapper.component.ts +315 -0
  89. package/tsconfig.storybook.json +10 -0
  90. package/vitest.config.ts +9 -9
  91. package/demo-app/README.md +0 -70
  92. package/demo-app/angular.json +0 -78
  93. package/demo-app/e2e/benchmark.spec.ts +0 -53
  94. package/demo-app/e2e/demo-page.spec.ts +0 -77
  95. package/demo-app/e2e/grid-features.spec.ts +0 -269
  96. package/demo-app/package-lock.json +0 -14023
  97. package/demo-app/package.json +0 -36
  98. package/demo-app/playwright-test-menu.js +0 -19
  99. package/demo-app/playwright.config.ts +0 -23
  100. package/demo-app/src/app/app.component.ts +0 -10
  101. package/demo-app/src/app/app.config.ts +0 -13
  102. package/demo-app/src/app/app.routes.ts +0 -7
  103. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  104. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  105. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  106. package/demo-app/src/index.html +0 -19
  107. package/demo-app/src/main.ts +0 -6
  108. package/demo-app/tsconfig.json +0 -31
@@ -1,7 +1,7 @@
1
- import { TestBed } from '@angular/core/testing';
2
1
  import { provideExperimentalZonelessChangeDetection } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { ColDef, FilterModel, GridApi, GridState } from '../types/ag-grid-types';
3
4
  import { GridService } from './grid.service';
4
- import { GridApi, ColDef, FilterModel, IRowNode } from '../types/ag-grid-types';
5
5
 
6
6
  interface TestData {
7
7
  id: number;
@@ -13,26 +13,23 @@ interface TestData {
13
13
  describe('GridService', () => {
14
14
  let service: GridService<TestData>;
15
15
  let api: GridApi<TestData>;
16
-
17
- const testColumnDefs: (ColDef<TestData>)[] = [
16
+
17
+ const testColumnDefs: ColDef<TestData>[] = [
18
18
  { colId: 'id', field: 'id', headerName: 'ID', width: 100 },
19
19
  { colId: 'name', field: 'name', headerName: 'Name', width: 150 },
20
20
  { colId: 'age', field: 'age', headerName: 'Age', width: 80, sortable: true },
21
- { colId: 'email', field: 'email', headerName: 'Email', width: 200 }
21
+ { colId: 'email', field: 'email', headerName: 'Email', width: 200 },
22
22
  ];
23
-
23
+
24
24
  const testRowData: TestData[] = [
25
25
  { id: 1, name: 'John Doe', age: 30, email: 'john@example.com' },
26
26
  { id: 2, name: 'Jane Smith', age: 25, email: 'jane@example.com' },
27
- { id: 3, name: 'Bob Johnson', age: 35, email: 'bob@example.com' }
27
+ { id: 3, name: 'Bob Johnson', age: 35, email: 'bob@example.com' },
28
28
  ];
29
29
 
30
30
  beforeEach(() => {
31
31
  TestBed.configureTestingModule({
32
- providers: [
33
- GridService,
34
- provideExperimentalZonelessChangeDetection()
35
- ]
32
+ providers: [GridService, provideExperimentalZonelessChangeDetection()],
36
33
  });
37
34
  service = TestBed.inject(GridService);
38
35
  api = service.createApi(testColumnDefs, [...testRowData]);
@@ -74,7 +71,7 @@ describe('GridService', () => {
74
71
  const sortApi = service.createApi(testColumnDefs, [...testRowData]);
75
72
  // Sort by age descending
76
73
  sortApi.setSortModel([{ colId: 'age', sort: 'desc' }]);
77
-
74
+
78
75
  const sortedData = sortApi.getRowData();
79
76
  expect(sortedData[0].age).toBe(35);
80
77
  expect(sortedData[2].age).toBe(25);
@@ -84,7 +81,7 @@ describe('GridService', () => {
84
81
  const sortApi = service.createApi(testColumnDefs, [...testRowData]);
85
82
  // Sort by age ascending
86
83
  sortApi.setSortModel([{ colId: 'age', sort: 'asc' }]);
87
-
84
+
88
85
  const sortedData = sortApi.getRowData();
89
86
  expect(sortedData[0].age).toBe(25);
90
87
  expect(sortedData[2].age).toBe(35);
@@ -93,14 +90,14 @@ describe('GridService', () => {
93
90
  it('should handle transaction - add rows and respect sorting', () => {
94
91
  const sortApi = service.createApi(testColumnDefs, [...testRowData]);
95
92
  sortApi.setSortModel([{ colId: 'name', sort: 'asc' }]);
96
-
93
+
97
94
  // Initial alpha: Bob Johnson (35), Jane Smith (25), John Doe (30)
98
95
  expect(sortApi.getDisplayedRowAtIndex(0)?.data.name).toBe('Bob Johnson');
99
96
 
100
97
  sortApi.applyTransaction({
101
- add: [{ id: 4, name: 'Alice', age: 28, email: 'alice@example.com' }]
98
+ add: [{ id: 4, name: 'Alice', age: 28, email: 'alice@example.com' }],
102
99
  });
103
-
100
+
104
101
  // Alice should now be first
105
102
  expect(sortApi.getDisplayedRowAtIndex(0)?.data.name).toBe('Alice');
106
103
  expect(sortApi.getDisplayedRowCount()).toBe(4);
@@ -109,14 +106,14 @@ describe('GridService', () => {
109
106
  it('should handle transaction - update rows and respect filtering', () => {
110
107
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
111
108
  filterApi.setFilterModel({
112
- age: { filterType: 'number', type: 'greaterThan', filter: 30 }
109
+ age: { filterType: 'number', type: 'greaterThan', filter: 30 },
113
110
  });
114
-
111
+
115
112
  expect(filterApi.getDisplayedRowCount()).toBe(1); // Only Bob (35)
116
113
 
117
114
  // Update Jane (25) to be 40
118
115
  filterApi.applyTransaction({
119
- update: [{ id: 2, name: 'Jane Smith', age: 40, email: 'jane@example.com' }]
116
+ update: [{ id: 2, name: 'Jane Smith', age: 40, email: 'jane@example.com' }],
120
117
  });
121
118
 
122
119
  expect(filterApi.getDisplayedRowCount()).toBe(2); // Bob and Jane
@@ -127,9 +124,14 @@ describe('GridService', () => {
127
124
  const firstNode = api.getDisplayedRowAtIndex(0);
128
125
  if (!firstNode || !firstNode.id) return;
129
126
 
130
- const removeData: TestData = { id: firstNode.data.id, name: firstNode.data.name, age: firstNode.data.age, email: firstNode.data.email };
127
+ const removeData: TestData = {
128
+ id: firstNode.data.id,
129
+ name: firstNode.data.name,
130
+ age: firstNode.data.age,
131
+ email: firstNode.data.email,
132
+ };
131
133
  const result = api.applyTransaction({
132
- remove: [removeData]
134
+ remove: [removeData],
133
135
  });
134
136
 
135
137
  expect(result?.remove.length).toBe(1);
@@ -138,7 +140,7 @@ describe('GridService', () => {
138
140
 
139
141
  it('should get and set filter model', () => {
140
142
  const filterModel: FilterModel = {
141
- name: { filterType: 'text', type: 'contains', filter: 'John' }
143
+ name: { filterType: 'text', type: 'contains', filter: 'John' },
142
144
  };
143
145
 
144
146
  api.setFilterModel(filterModel);
@@ -149,7 +151,7 @@ describe('GridService', () => {
149
151
  it('should apply text filter - contains', () => {
150
152
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
151
153
  filterApi.setFilterModel({
152
- name: { filterType: 'text', type: 'contains', filter: 'John' }
154
+ name: { filterType: 'text', type: 'contains', filter: 'John' },
153
155
  });
154
156
 
155
157
  const data = filterApi.getRowData();
@@ -160,12 +162,12 @@ describe('GridService', () => {
160
162
  it('should apply text filter - starts with', () => {
161
163
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
162
164
  filterApi.setFilterModel({
163
- name: { filterType: 'text', type: 'startsWith', filter: 'J' }
165
+ name: { filterType: 'text', type: 'startsWith', filter: 'J' },
164
166
  });
165
167
 
166
168
  const data = filterApi.getRowData();
167
169
  // Should match 'John Doe' and 'Jane Smith'
168
- data.forEach(row => {
170
+ data.forEach((row) => {
169
171
  expect(row.name.startsWith('J')).toBe(true);
170
172
  });
171
173
  });
@@ -173,11 +175,11 @@ describe('GridService', () => {
173
175
  it('should apply text filter - ends with', () => {
174
176
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
175
177
  filterApi.setFilterModel({
176
- name: { filterType: 'text', type: 'endsWith', filter: 'e' }
178
+ name: { filterType: 'text', type: 'endsWith', filter: 'e' },
177
179
  });
178
180
 
179
181
  const data = filterApi.getRowData();
180
- data.forEach(row => {
182
+ data.forEach((row) => {
181
183
  expect(row.name.endsWith('e')).toBe(true);
182
184
  });
183
185
  });
@@ -185,7 +187,7 @@ describe('GridService', () => {
185
187
  it('should apply text filter - equals', () => {
186
188
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
187
189
  filterApi.setFilterModel({
188
- name: { filterType: 'text', type: 'equals', filter: 'John Doe' }
190
+ name: { filterType: 'text', type: 'equals', filter: 'John Doe' },
189
191
  });
190
192
 
191
193
  const data = filterApi.getRowData();
@@ -196,11 +198,11 @@ describe('GridService', () => {
196
198
  it('should apply number filter - greater than', () => {
197
199
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
198
200
  filterApi.setFilterModel({
199
- age: { filterType: 'number', type: 'greaterThan', filter: 28 }
201
+ age: { filterType: 'number', type: 'greaterThan', filter: 28 },
200
202
  });
201
203
 
202
204
  const data = filterApi.getRowData();
203
- data.forEach(row => {
205
+ data.forEach((row) => {
204
206
  expect(row.age).toBeGreaterThan(28);
205
207
  });
206
208
  });
@@ -208,11 +210,11 @@ describe('GridService', () => {
208
210
  it('should apply number filter - less than', () => {
209
211
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
210
212
  filterApi.setFilterModel({
211
- age: { filterType: 'number', type: 'lessThan', filter: 32 }
213
+ age: { filterType: 'number', type: 'lessThan', filter: 32 },
212
214
  });
213
215
 
214
216
  const data = filterApi.getRowData();
215
- data.forEach(row => {
217
+ data.forEach((row) => {
216
218
  expect(row.age).toBeLessThan(32);
217
219
  });
218
220
  });
@@ -220,11 +222,11 @@ describe('GridService', () => {
220
222
  it('should apply number filter - in range', () => {
221
223
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
222
224
  filterApi.setFilterModel({
223
- age: { filterType: 'number', type: 'inRange', filter: 26, filterTo: 34 }
225
+ age: { filterType: 'number', type: 'inRange', filter: 26, filterTo: 34 },
224
226
  });
225
227
 
226
228
  const data = filterApi.getRowData();
227
- data.forEach(row => {
229
+ data.forEach((row) => {
228
230
  expect(row.age).toBeGreaterThanOrEqual(26);
229
231
  expect(row.age).toBeLessThanOrEqual(34);
230
232
  });
@@ -234,17 +236,17 @@ describe('GridService', () => {
234
236
  const dateData: any[] = [
235
237
  { id: 1, name: 'Event 1', date: '2024-01-15' },
236
238
  { id: 2, name: 'Event 2', date: '2024-06-20' },
237
- { id: 3, name: 'Event 3', date: '2024-12-01' }
239
+ { id: 3, name: 'Event 3', date: '2024-12-01' },
238
240
  ];
239
241
  const dateColumnDefs: any[] = [
240
242
  { colId: 'id', field: 'id', headerName: 'ID' },
241
243
  { colId: 'name', field: 'name', headerName: 'Name' },
242
- { colId: 'date', field: 'date', headerName: 'Date', filter: 'agDateColumnFilter' }
244
+ { colId: 'date', field: 'date', headerName: 'Date', filter: 'agDateColumnFilter' },
243
245
  ];
244
246
 
245
247
  const filterApi = service.createApi(dateColumnDefs, dateData);
246
248
  filterApi.setFilterModel({
247
- date: { filterType: 'date', type: 'greaterThan', filter: '2024-03-01' }
249
+ date: { filterType: 'date', type: 'greaterThan', filter: '2024-03-01' },
248
250
  });
249
251
 
250
252
  const data = filterApi.getRowData();
@@ -255,7 +257,7 @@ describe('GridService', () => {
255
257
  it('should clear filter when model is empty', () => {
256
258
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
257
259
  filterApi.setFilterModel({
258
- name: { filterType: 'text', type: 'contains', filter: 'John' }
260
+ name: { filterType: 'text', type: 'contains', filter: 'John' },
259
261
  });
260
262
  expect(api.isFilterPresent()).toBe(true);
261
263
 
@@ -267,12 +269,12 @@ describe('GridService', () => {
267
269
  const filterApi = service.createApi(testColumnDefs, [...testRowData]);
268
270
  filterApi.setFilterModel({
269
271
  name: { filterType: 'text', type: 'startsWith', filter: 'J' },
270
- age: { filterType: 'number', type: 'lessThan', filter: 30 }
272
+ age: { filterType: 'number', type: 'lessThan', filter: 30 },
271
273
  });
272
274
 
273
275
  const data = filterApi.getRowData();
274
276
  // Should match 'Jane Smith' (starts with J and age < 30)
275
- data.forEach(row => {
277
+ data.forEach((row) => {
276
278
  expect(row.name.startsWith('J')).toBe(true);
277
279
  expect(row.age).toBeLessThan(30);
278
280
  });
@@ -284,16 +286,16 @@ describe('GridService', () => {
284
286
  { id: 1, name: 'John', department: 'Engineering', salary: 80000 },
285
287
  { id: 2, name: 'Jane', department: 'Engineering', salary: 90000 },
286
288
  { id: 3, name: 'Bob', department: 'Sales', salary: 70000 },
287
- { id: 4, name: 'Alice', department: 'Sales', salary: 75000 }
289
+ { id: 4, name: 'Alice', department: 'Sales', salary: 75000 },
288
290
  ];
289
291
  const groupColumnDefs: ColDef[] = [
290
292
  { colId: 'name', field: 'name', headerName: 'Name' },
291
293
  { colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
292
- { colId: 'salary', field: 'salary', headerName: 'Salary' }
294
+ { colId: 'salary', field: 'salary', headerName: 'Salary' },
293
295
  ];
294
296
 
295
297
  const groupApi = service.createApi(groupColumnDefs, groupData);
296
-
298
+
297
299
  // With groups collapsed, should show 2 group rows
298
300
  const displayedCount = groupApi.getDisplayedRowCount();
299
301
  expect(displayedCount).toBe(2); // 2 groups (Engineering, Sales)
@@ -303,17 +305,17 @@ describe('GridService', () => {
303
305
  const groupData: any[] = [
304
306
  { id: 1, name: 'John', department: 'Engineering' },
305
307
  { id: 2, name: 'Jane', department: 'Engineering' },
306
- { id: 3, name: 'Bob', department: 'Sales' }
308
+ { id: 3, name: 'Bob', department: 'Sales' },
307
309
  ];
308
310
  const groupColumnDefs: ColDef[] = [
309
311
  { colId: 'name', field: 'name', headerName: 'Name' },
310
- { colId: 'department', field: 'department', headerName: 'Department', rowGroup: true }
312
+ { colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
311
313
  ];
312
314
 
313
315
  const groupApi = service.createApi(groupColumnDefs, groupData);
314
-
316
+
315
317
  // Initially groups are collapsed
316
- let displayedCount = groupApi.getDisplayedRowCount();
318
+ const displayedCount = groupApi.getDisplayedRowCount();
317
319
  expect(displayedCount).toBe(2); // 2 groups
318
320
  });
319
321
 
@@ -321,16 +323,16 @@ describe('GridService', () => {
321
323
  const groupData: any[] = [
322
324
  { id: 1, name: 'John', department: 'Engineering', salary: 80000 },
323
325
  { id: 2, name: 'Jane', department: 'Engineering', salary: 90000 },
324
- { id: 3, name: 'Bob', department: 'Sales', salary: 70000 }
326
+ { id: 3, name: 'Bob', department: 'Sales', salary: 70000 },
325
327
  ];
326
328
  const groupColumnDefs: ColDef[] = [
327
329
  { colId: 'name', field: 'name', headerName: 'Name' },
328
330
  { colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
329
- { colId: 'salary', field: 'salary', headerName: 'Salary', aggFunc: 'sum' }
331
+ { colId: 'salary', field: 'salary', headerName: 'Salary', aggFunc: 'sum' },
330
332
  ];
331
333
 
332
334
  const groupApi = service.createApi(groupColumnDefs, groupData);
333
-
335
+
334
336
  // Verify grouping works
335
337
  const displayedCount = groupApi.getDisplayedRowCount();
336
338
  expect(displayedCount).toBeGreaterThanOrEqual(2); // At least 2 groups
@@ -340,16 +342,16 @@ describe('GridService', () => {
340
342
  const groupData: any[] = [
341
343
  { id: 1, name: 'John', department: 'Engineering', level: 'Senior' },
342
344
  { id: 2, name: 'Jane', department: 'Engineering', level: 'Junior' },
343
- { id: 3, name: 'Bob', department: 'Sales', level: 'Senior' }
345
+ { id: 3, name: 'Bob', department: 'Sales', level: 'Senior' },
344
346
  ];
345
347
  const groupColumnDefs: ColDef[] = [
346
348
  { colId: 'name', field: 'name', headerName: 'Name' },
347
349
  { colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
348
- { colId: 'level', field: 'level', headerName: 'Level', rowGroup: true }
350
+ { colId: 'level', field: 'level', headerName: 'Level', rowGroup: true },
349
351
  ];
350
352
 
351
353
  const groupApi = service.createApi(groupColumnDefs, groupData);
352
-
354
+
353
355
  // Should have hierarchical groups (Engineering/Senior, Engineering/Junior, Sales/Senior)
354
356
  const displayedCount = groupApi.getDisplayedRowCount();
355
357
  expect(displayedCount).toBe(2); // Engineering and Sales top level
@@ -368,11 +370,11 @@ describe('GridService', () => {
368
370
  if (!firstNode) return;
369
371
 
370
372
  const originalName = firstNode.data.name;
371
-
373
+
372
374
  // Simulate cell edit via transaction
373
375
  const newValue = 'Updated Name';
374
376
  editApi.applyTransaction({
375
- update: [{ ...firstNode.data, name: newValue }]
377
+ update: [{ ...firstNode.data, name: newValue }],
376
378
  });
377
379
 
378
380
  const updatedNode = editApi.getDisplayedRowAtIndex(0);
@@ -383,9 +385,9 @@ describe('GridService', () => {
383
385
  it('should support read-only cells', () => {
384
386
  const readOnlyColumnDefs: ColDef[] = [
385
387
  { colId: 'id', field: 'id', headerName: 'ID', editable: false },
386
- { colId: 'name', field: 'name', headerName: 'Name', editable: true }
388
+ { colId: 'name', field: 'name', headerName: 'Name', editable: true },
387
389
  ];
388
-
390
+
389
391
  expect(readOnlyColumnDefs[0].editable).toBe(false);
390
392
  expect(readOnlyColumnDefs[1].editable).toBe(true);
391
393
  });
@@ -393,23 +395,31 @@ describe('GridService', () => {
393
395
  it('should support valueParser on column', () => {
394
396
  const parserColumnDefs: ColDef[] = [
395
397
  { colId: 'id', field: 'id', headerName: 'ID' },
396
- { colId: 'value', field: 'value', headerName: 'Value',
397
- valueParser: (params: any) => Number(params.newValue) }
398
+ {
399
+ colId: 'value',
400
+ field: 'value',
401
+ headerName: 'Value',
402
+ valueParser: (params: any) => Number(params.newValue),
403
+ },
398
404
  ];
399
-
405
+
400
406
  expect(parserColumnDefs[1].valueParser).toBeDefined();
401
407
  });
402
408
 
403
409
  it('should support valueSetter on column', () => {
404
410
  const setterColumnDefs: ColDef[] = [
405
411
  { colId: 'id', field: 'id', headerName: 'ID' },
406
- { colId: 'name', field: 'name', headerName: 'Name',
412
+ {
413
+ colId: 'name',
414
+ field: 'name',
415
+ headerName: 'Name',
407
416
  valueSetter: (params: any) => {
408
417
  params.data.name = params.newValue.toUpperCase();
409
418
  return true;
410
- }}
419
+ },
420
+ },
411
421
  ];
412
-
422
+
413
423
  expect(setterColumnDefs[1].valueSetter).toBeDefined();
414
424
  });
415
425
 
@@ -418,13 +428,13 @@ describe('GridService', () => {
418
428
  const pinColumnDefs: ColDef[] = [
419
429
  { colId: 'id', field: 'id', headerName: 'ID', pinned: 'left' },
420
430
  { colId: 'name', field: 'name', headerName: 'Name' },
421
- { colId: 'value', field: 'value', headerName: 'Value' }
431
+ { colId: 'value', field: 'value', headerName: 'Value' },
422
432
  ];
423
-
433
+
424
434
  const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
425
435
  const columns = pinApi.getAllColumns();
426
-
427
- const pinnedCol = columns.find(c => c.pinned === 'left');
436
+
437
+ const pinnedCol = columns.find((c) => c.pinned === 'left');
428
438
  expect(pinnedCol).toBeDefined();
429
439
  expect(pinnedCol?.colId).toBe('id');
430
440
  });
@@ -433,13 +443,13 @@ describe('GridService', () => {
433
443
  const pinColumnDefs: ColDef[] = [
434
444
  { colId: 'id', field: 'id', headerName: 'ID' },
435
445
  { colId: 'name', field: 'name', headerName: 'Name' },
436
- { colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' }
446
+ { colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' },
437
447
  ];
438
-
448
+
439
449
  const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
440
450
  const columns = pinApi.getAllColumns();
441
-
442
- const pinnedCol = columns.find(c => c.pinned === 'right');
451
+
452
+ const pinnedCol = columns.find((c) => c.pinned === 'right');
443
453
  expect(pinnedCol).toBeDefined();
444
454
  expect(pinnedCol?.colId).toBe('value');
445
455
  });
@@ -448,12 +458,12 @@ describe('GridService', () => {
448
458
  const pinColumnDefs: ColDef[] = [
449
459
  { colId: 'id', field: 'id', headerName: 'ID', pinned: 'left' },
450
460
  { colId: 'name', field: 'name', headerName: 'Name' },
451
- { colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' }
461
+ { colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' },
452
462
  ];
453
-
463
+
454
464
  const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
455
465
  const state = pinApi.getState();
456
-
466
+
457
467
  expect(state.columnPinning).toBeDefined();
458
468
  expect(state.columnPinning?.left).toContain('id');
459
469
  expect(state.columnPinning?.right).toContain('value');
@@ -464,15 +474,15 @@ describe('GridService', () => {
464
474
  const rowData: any[] = [
465
475
  { id: 1, name: 'Row 1' },
466
476
  { id: 2, name: 'Row 2', pinned: 'top' },
467
- { id: 3, name: 'Row 3' }
477
+ { id: 3, name: 'Row 3' },
468
478
  ];
469
-
479
+
470
480
  const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
471
481
  const displayedCount = pinApi.getDisplayedRowCount();
472
-
482
+
473
483
  // Should have 3 rows total
474
484
  expect(displayedCount).toBe(3);
475
-
485
+
476
486
  // First row should be the pinned top one
477
487
  const firstRow = pinApi.getDisplayedRowAtIndex(0);
478
488
  expect(firstRow?.rowPinned).toBe('top');
@@ -482,11 +492,11 @@ describe('GridService', () => {
482
492
  const rowData: any[] = [
483
493
  { id: 1, name: 'Row 1' },
484
494
  { id: 2, name: 'Row 2', pinned: 'bottom' },
485
- { id: 3, name: 'Row 3' }
495
+ { id: 3, name: 'Row 3' },
486
496
  ];
487
-
497
+
488
498
  const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
489
-
499
+
490
500
  // Last row should be the pinned bottom one
491
501
  const lastRowIndex = pinApi.getDisplayedRowCount() - 1;
492
502
  const lastRow = pinApi.getDisplayedRowAtIndex(lastRowIndex);
@@ -499,19 +509,19 @@ describe('GridService', () => {
499
509
  { id: 2, name: 'Top 1', pinned: 'top' },
500
510
  { id: 3, name: 'Normal 2' },
501
511
  { id: 4, name: 'Bottom 1', pinned: 'bottom' },
502
- { id: 5, name: 'Top 2', pinned: 'top' }
512
+ { id: 5, name: 'Top 2', pinned: 'top' },
503
513
  ];
504
-
514
+
505
515
  const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
506
-
516
+
507
517
  // Pinned top rows should come first
508
518
  expect(pinApi.getDisplayedRowAtIndex(0)?.rowPinned).toBe('top');
509
519
  expect(pinApi.getDisplayedRowAtIndex(1)?.rowPinned).toBe('top');
510
-
520
+
511
521
  // Normal rows in middle
512
522
  expect(pinApi.getDisplayedRowAtIndex(2)?.rowPinned).toBe(false);
513
523
  expect(pinApi.getDisplayedRowAtIndex(3)?.rowPinned).toBe(false);
514
-
524
+
515
525
  // Pinned bottom rows at end
516
526
  expect(pinApi.getDisplayedRowAtIndex(4)?.rowPinned).toBe('bottom');
517
527
  });
@@ -526,27 +536,27 @@ describe('GridService', () => {
526
536
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
527
537
  const firstRow = selectApi.getDisplayedRowAtIndex(0);
528
538
  if (!firstRow) return;
529
-
539
+
530
540
  firstRow.selected = true;
531
541
  const selected = selectApi.getSelectedRows();
532
-
542
+
533
543
  expect(selected.length).toBe(1);
534
544
  expect(selected[0].data.id).toBe(1);
535
545
  });
536
546
 
537
547
  it('should select multiple rows with Ctrl key', () => {
538
548
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
539
-
549
+
540
550
  // Select first row
541
551
  const firstRow = selectApi.getDisplayedRowAtIndex(0);
542
552
  if (!firstRow) return;
543
553
  firstRow.selected = true;
544
-
554
+
545
555
  // Ctrl+click to select third row (multi-select)
546
556
  const thirdRow = selectApi.getDisplayedRowAtIndex(2);
547
557
  if (!thirdRow) return;
548
558
  thirdRow.selected = true;
549
-
559
+
550
560
  const selected = selectApi.getSelectedRows();
551
561
  expect(selected.length).toBe(2);
552
562
  });
@@ -554,7 +564,7 @@ describe('GridService', () => {
554
564
  it('should select all rows', () => {
555
565
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
556
566
  selectApi.selectAll();
557
-
567
+
558
568
  const selected = selectApi.getSelectedRows();
559
569
  expect(selected.length).toBe(3);
560
570
  });
@@ -563,7 +573,7 @@ describe('GridService', () => {
563
573
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
564
574
  selectApi.selectAll();
565
575
  expect(selectApi.getSelectedRows().length).toBe(3);
566
-
576
+
567
577
  selectApi.deselectAll();
568
578
  expect(selectApi.getSelectedRows().length).toBe(0);
569
579
  });
@@ -572,10 +582,10 @@ describe('GridService', () => {
572
582
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
573
583
  const row = selectApi.getDisplayedRowAtIndex(0);
574
584
  if (!row) return;
575
-
585
+
576
586
  row.selected = true;
577
587
  expect(selectApi.getSelectedRows().length).toBe(1);
578
-
588
+
579
589
  row.selected = false;
580
590
  expect(selectApi.getSelectedRows().length).toBe(0);
581
591
  });
@@ -583,19 +593,50 @@ describe('GridService', () => {
583
593
  it('should get selected row count', () => {
584
594
  const selectApi = service.createApi(testColumnDefs, [...testRowData]);
585
595
  selectApi.selectAll();
586
-
596
+
587
597
  const selectedCount = selectApi.getSelectedRows().length;
588
598
  expect(selectedCount).toBe(3);
589
599
  });
590
600
 
591
601
  it('should support row selection with checkbox', () => {
592
602
  const selectionColumnDefs: ColDef[] = [
593
- { colId: 'select', headerName: '', checkboxSelection: true, width: 50 },
594
- { colId: 'id', field: 'id', headerName: 'ID' },
595
- { colId: 'name', field: 'name', headerName: 'Name' }
603
+ { colId: 'id', field: 'id', headerName: 'ID', checkboxSelection: true },
604
+ { colId: 'name', field: 'name', headerName: 'Name' },
596
605
  ];
597
-
598
- expect(selectionColumnDefs[0].checkboxSelection).toBe(true);
606
+
607
+ const selectionApi = service.createApi(selectionColumnDefs, [...testRowData]);
608
+ const columns = selectionApi.getAllColumns();
609
+
610
+ // Should have 3 columns: Dedicated Selection + ID + Name
611
+ expect(columns.length).toBe(3);
612
+ expect(columns[0].colId).toBe('ag-Grid-SelectionColumn');
613
+ expect(columns[0].pinned).toBe('left');
614
+ expect(columns[0].width).toBe(50);
615
+ });
616
+
617
+ describe('Dedicated Selection Column', () => {
618
+ it('should be created if checkboxSelection is true in any column', () => {
619
+ const defs: ColDef[] = [{ field: 'id', checkboxSelection: true }];
620
+ const api = service.createApi(defs, []);
621
+ expect(api.getAllColumns().some((c) => c.colId === 'ag-Grid-SelectionColumn')).toBe(true);
622
+ });
623
+
624
+ it('should be created if checkboxSelection is true in a nested column', () => {
625
+ const defs: any[] = [
626
+ {
627
+ headerName: 'Group',
628
+ children: [{ field: 'id', checkboxSelection: true }],
629
+ },
630
+ ];
631
+ const api = service.createApi(defs, []);
632
+ expect(api.getAllColumns().some((c) => c.colId === 'ag-Grid-SelectionColumn')).toBe(true);
633
+ });
634
+
635
+ it('should not be created if no checkboxSelection is present', () => {
636
+ const defs: ColDef[] = [{ field: 'id' }];
637
+ const api = service.createApi(defs, []);
638
+ expect(api.getAllColumns().some((c) => c.colId === 'ag-Grid-SelectionColumn')).toBe(false);
639
+ });
599
640
  });
600
641
 
601
642
  // Aggregation Tests
@@ -603,112 +644,112 @@ describe('GridService', () => {
603
644
  const aggData: any[] = [
604
645
  { id: 1, name: 'Item 1', value: 100 },
605
646
  { id: 2, name: 'Item 2', value: 200 },
606
- { id: 3, name: 'Item 3', value: 300 }
647
+ { id: 3, name: 'Item 3', value: 300 },
607
648
  ];
608
649
  const aggColumnDefs: ColDef[] = [
609
650
  { colId: 'id', field: 'id', headerName: 'ID' },
610
651
  { colId: 'name', field: 'name', headerName: 'Name' },
611
- { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'sum' }
652
+ { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'sum' },
612
653
  ];
613
-
654
+
614
655
  service.createApi(aggColumnDefs, aggData);
615
656
  const agg = service.calculateColumnAggregations(aggData);
616
- expect(agg['value']).toBe(600);
657
+ expect(agg.value).toBe(600);
617
658
  });
618
659
 
619
660
  it('should calculate average aggregation', () => {
620
661
  const aggData: any[] = [
621
662
  { id: 1, name: 'Item 1', value: 100 },
622
663
  { id: 2, name: 'Item 2', value: 200 },
623
- { id: 3, name: 'Item 3', value: 300 }
664
+ { id: 3, name: 'Item 3', value: 300 },
624
665
  ];
625
666
  const aggColumnDefs: ColDef[] = [
626
667
  { colId: 'id', field: 'id', headerName: 'ID' },
627
- { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'avg' }
668
+ { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'avg' },
628
669
  ];
629
-
670
+
630
671
  service.createApi(aggColumnDefs, aggData);
631
672
  const agg = service.calculateColumnAggregations(aggData);
632
- expect(agg['value']).toBe(200);
673
+ expect(agg.value).toBe(200);
633
674
  });
634
675
 
635
676
  it('should calculate min/max aggregation', () => {
636
677
  const aggData: any[] = [
637
678
  { id: 1, name: 'Item 1', value: 100 },
638
679
  { id: 2, name: 'Item 2', value: 50 },
639
- { id: 3, name: 'Item 3', value: 300 }
680
+ { id: 3, name: 'Item 3', value: 300 },
640
681
  ];
641
682
  const aggColumnDefs: ColDef[] = [
642
- { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'min' }
683
+ { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'min' },
643
684
  ];
644
-
685
+
645
686
  service.createApi(aggColumnDefs, aggData);
646
687
  const agg = service.calculateColumnAggregations(aggData);
647
- expect(agg['value']).toBe(50);
688
+ expect(agg.value).toBe(50);
648
689
  });
649
690
 
650
691
  it('should calculate max aggregation', () => {
651
692
  const aggData: any[] = [
652
693
  { id: 1, name: 'Item 1', value: 100 },
653
694
  { id: 2, name: 'Item 2', value: 50 },
654
- { id: 3, name: 'Item 3', value: 300 }
695
+ { id: 3, name: 'Item 3', value: 300 },
655
696
  ];
656
697
  const aggColumnDefs: ColDef[] = [
657
- { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'max' }
698
+ { colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'max' },
658
699
  ];
659
-
700
+
660
701
  service.createApi(aggColumnDefs, aggData);
661
702
  const agg = service.calculateColumnAggregations(aggData);
662
- expect(agg['value']).toBe(300);
703
+ expect(agg.value).toBe(300);
663
704
  });
664
705
 
665
706
  it('should calculate count aggregation', () => {
666
707
  const aggData: any[] = [
667
708
  { id: 1, name: 'Item 1' },
668
709
  { id: 2, name: 'Item 2' },
669
- { id: 3, name: 'Item 3' }
710
+ { id: 3, name: 'Item 3' },
670
711
  ];
671
712
  const aggColumnDefs: ColDef[] = [
672
- { colId: 'id', field: 'id', headerName: 'ID', aggFunc: 'count' }
713
+ { colId: 'id', field: 'id', headerName: 'ID', aggFunc: 'count' },
673
714
  ];
674
-
715
+
675
716
  service.createApi(aggColumnDefs, aggData);
676
717
  const agg = service.calculateColumnAggregations(aggData);
677
- expect(agg['id']).toBe(3);
718
+ expect(agg.id).toBe(3);
678
719
  });
679
720
 
680
721
  it('should support custom aggregation function', () => {
681
722
  const aggData: any[] = [
682
723
  { id: 1, value: 100 },
683
724
  { id: 2, value: 200 },
684
- { id: 3, value: 300 }
725
+ { id: 3, value: 300 },
685
726
  ];
686
727
  const customAggFunc = (params: any) => {
687
728
  return params.values.reduce((sum: number, v: number) => sum + v, 0) * 2;
688
729
  };
689
730
  const aggColumnDefs: ColDef[] = [
690
- { colId: 'value', field: 'value', headerName: 'Value', aggFunc: customAggFunc }
731
+ { colId: 'value', field: 'value', headerName: 'Value', aggFunc: customAggFunc },
691
732
  ];
692
-
733
+
693
734
  service.createApi(aggColumnDefs, aggData);
694
735
  const agg = service.calculateColumnAggregations(aggData);
695
- expect(agg['value']).toBe(1200); // (100+200+300) * 2
736
+ expect(agg.value).toBe(1200); // (100+200+300) * 2
696
737
  });
697
738
 
698
739
  // Excel Export Tests
699
740
  it('should export data as CSV', () => {
700
741
  const exportData: any[] = [
701
742
  { id: 1, name: 'John', email: 'john@example.com' },
702
- { id: 2, name: 'Jane', email: 'jane@example.com' }
743
+ { id: 2, name: 'Jane', email: 'jane@example.com' },
703
744
  ];
704
745
  const exportColumnDefs: ColDef[] = [
705
746
  { colId: 'id', field: 'id', headerName: 'ID' },
706
747
  { colId: 'name', field: 'name', headerName: 'Name' },
707
- { colId: 'email', field: 'email', headerName: 'Email' }
748
+ { colId: 'email', field: 'email', headerName: 'Email' },
708
749
  ];
709
-
750
+
710
751
  const exportApi = service.createApi(exportColumnDefs, exportData);
711
-
752
+
712
753
  // Mock downloadFile to avoid browser API issues in tests
713
754
  (service as any).downloadFile = vi.fn();
714
755
  expect(() => exportApi.exportDataAsCsv()).not.toThrow();
@@ -719,12 +760,12 @@ describe('GridService', () => {
719
760
  const exportData: any[] = [{ id: 1, name: 'Test' }];
720
761
  const exportColumnDefs: ColDef[] = [
721
762
  { colId: 'id', field: 'id', headerName: 'ID' },
722
- { colId: 'name', field: 'name', headerName: 'Name' }
763
+ { colId: 'name', field: 'name', headerName: 'Name' },
723
764
  ];
724
-
765
+
725
766
  const exportApi = service.createApi(exportColumnDefs, exportData);
726
767
  (service as any).downloadFile = vi.fn();
727
-
768
+
728
769
  exportApi.exportDataAsCsv({ fileName: 'custom-export.csv' });
729
770
  expect((service as any).downloadFile).toHaveBeenCalledWith(
730
771
  expect.any(String),
@@ -736,17 +777,17 @@ describe('GridService', () => {
736
777
  it('should export only selected columns', () => {
737
778
  const exportData: any[] = [
738
779
  { id: 1, name: 'John', email: 'john@example.com' },
739
- { id: 2, name: 'Jane', email: 'jane@example.com' }
780
+ { id: 2, name: 'Jane', email: 'jane@example.com' },
740
781
  ];
741
782
  const exportColumnDefs: ColDef[] = [
742
783
  { colId: 'id', field: 'id', headerName: 'ID' },
743
784
  { colId: 'name', field: 'name', headerName: 'Name' },
744
- { colId: 'email', field: 'email', headerName: 'Email' }
785
+ { colId: 'email', field: 'email', headerName: 'Email' },
745
786
  ];
746
-
787
+
747
788
  const exportApi = service.createApi(exportColumnDefs, exportData);
748
789
  (service as any).downloadFile = vi.fn();
749
-
790
+
750
791
  exportApi.exportDataAsCsv({ columnKeys: ['id', 'name'] });
751
792
  const csvContent = (service as any).downloadFile.mock.calls[0][0];
752
793
  // Should not contain email column
@@ -757,12 +798,12 @@ describe('GridService', () => {
757
798
  const exportData: any[] = [{ id: 1, name: 'Test' }];
758
799
  const exportColumnDefs: ColDef[] = [
759
800
  { colId: 'id', field: 'id', headerName: 'ID' },
760
- { colId: 'name', field: 'name', headerName: 'Name' }
801
+ { colId: 'name', field: 'name', headerName: 'Name' },
761
802
  ];
762
-
803
+
763
804
  const exportApi = service.createApi(exportColumnDefs, exportData);
764
805
  (service as any).downloadFile = vi.fn();
765
-
806
+
766
807
  exportApi.exportDataAsCsv({ skipHeader: true });
767
808
  const csvContent = (service as any).downloadFile.mock.calls[0][0];
768
809
  // First line should be data, not header
@@ -773,11 +814,11 @@ describe('GridService', () => {
773
814
  const exportData: any[] = [{ id: 1, name: 'Test' }];
774
815
  const exportColumnDefs: ColDef[] = [
775
816
  { colId: 'id', field: 'id', headerName: 'ID' },
776
- { colId: 'name', field: 'name', headerName: 'Name' }
817
+ { colId: 'name', field: 'name', headerName: 'Name' },
777
818
  ];
778
-
819
+
779
820
  const exportApi = service.createApi(exportColumnDefs, exportData);
780
-
821
+
781
822
  // Mock URL methods
782
823
  if (typeof URL.createObjectURL === 'undefined') {
783
824
  URL.createObjectURL = vi.fn().mockReturnValue('blob:test');
@@ -785,7 +826,7 @@ describe('GridService', () => {
785
826
  if (typeof URL.revokeObjectURL === 'undefined') {
786
827
  URL.revokeObjectURL = vi.fn();
787
828
  }
788
-
829
+
789
830
  // We expect it not to throw during the setup phase
790
831
  expect(() => exportApi.exportDataAsExcel()).not.toThrow();
791
832
  });
@@ -793,7 +834,7 @@ describe('GridService', () => {
793
834
  it('should get displayed row at index', () => {
794
835
  const sortedApi = service.createApi(testColumnDefs, [
795
836
  { id: 10, name: 'First', age: 20, email: 'first@example.com' },
796
- { id: 20, name: 'Second', age: 25, email: 'second@example.com' }
837
+ { id: 20, name: 'Second', age: 25, email: 'second@example.com' },
797
838
  ]);
798
839
  const row = sortedApi.getDisplayedRowAtIndex(1);
799
840
  expect(row).toBeTruthy();
@@ -821,12 +862,12 @@ describe('GridService', () => {
821
862
  it('should support custom getRowId in gridOptions', () => {
822
863
  const data = [
823
864
  { customId: 'A', name: 'John' },
824
- { customId: 'B', name: 'Jane' }
865
+ { customId: 'B', name: 'Jane' },
825
866
  ];
826
867
  const customApi = service.createApi(testColumnDefs, data, {
827
- getRowId: (params) => params.data.customId
868
+ getRowId: (params) => params.data.customId,
828
869
  });
829
-
870
+
830
871
  expect(customApi.getDisplayedRowCount()).toBe(2);
831
872
  expect(customApi.getRowNode('A')).toBeTruthy();
832
873
  expect(customApi.getRowNode('B')).toBeTruthy();
@@ -836,11 +877,11 @@ describe('GridService', () => {
836
877
  it('should handle rows with missing fields', () => {
837
878
  const data = [
838
879
  { id: 1, name: 'John' },
839
- { id: 2, age: 30 }
880
+ { id: 2, age: 30 },
840
881
  ];
841
882
  const missingApi = service.createApi(testColumnDefs, data);
842
883
  expect(missingApi.getDisplayedRowCount()).toBe(2);
843
-
884
+
844
885
  // Test sorting on missing field
845
886
  missingApi.setSortModel([{ colId: 'age', sort: 'asc' }]);
846
887
  // John (undefined age) should be at the end according to compareValues
@@ -851,12 +892,12 @@ describe('GridService', () => {
851
892
  it('should handle duplicate IDs (last one wins in map)', () => {
852
893
  const data = [
853
894
  { id: 'dup', name: 'First' },
854
- { id: 'dup', name: 'Second' }
895
+ { id: 'dup', name: 'Second' },
855
896
  ];
856
897
  const dupApi = service.createApi(testColumnDefs, data);
857
-
898
+
858
899
  expect(dupApi.getDisplayedRowCount()).toBe(2);
859
-
900
+
860
901
  const node = dupApi.getRowNode('dup');
861
902
  expect(node?.data.name).toBe('Second');
862
903
  });
@@ -864,15 +905,18 @@ describe('GridService', () => {
864
905
  it('should handle update transaction for non-existent row', () => {
865
906
  const api = service.createApi(testColumnDefs, [{ id: 1, name: 'John' }]);
866
907
  const result = api.applyTransaction({
867
- update: [{ id: 99, name: 'Missing' }]
908
+ update: [{ id: 99, name: 'Missing' }],
868
909
  });
869
-
910
+
870
911
  expect(result?.update.length).toBe(0);
871
912
  expect(api.getDisplayedRowCount()).toBe(1);
872
913
  });
873
914
 
874
915
  it('should handle sorting on non-existent column', () => {
875
- const api = service.createApi(testColumnDefs, [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]);
916
+ const api = service.createApi(testColumnDefs, [
917
+ { id: 1, name: 'John' },
918
+ { id: 2, name: 'Jane' },
919
+ ]);
876
920
  // Should not crash
877
921
  api.setSortModel([{ colId: 'invalid', sort: 'asc' }]);
878
922
  expect(api.getDisplayedRowCount()).toBe(2);
@@ -881,14 +925,14 @@ describe('GridService', () => {
881
925
  it('should preserve selection across transactions', () => {
882
926
  const api = service.createApi(testColumnDefs, [
883
927
  { id: 1, name: 'John' },
884
- { id: 2, name: 'Jane' }
928
+ { id: 2, name: 'Jane' },
885
929
  ]);
886
-
930
+
887
931
  const node1 = api.getRowNode('1')!;
888
932
  node1.selected = true;
889
-
933
+
890
934
  api.applyTransaction({ add: [{ id: 3, name: 'Bob' }] });
891
-
935
+
892
936
  const sameNode1 = api.getRowNode('1')!;
893
937
  expect(sameNode1.selected).toBe(true);
894
938
  expect(api.getSelectedNodes().length).toBe(1);
@@ -901,53 +945,58 @@ describe('GridService', () => {
901
945
  { field: 'name', headerName: 'Name' },
902
946
  { field: 'dept', headerName: 'Dept', rowGroup: true },
903
947
  { field: 'location', headerName: 'Location', pivot: true },
904
- { field: 'salary', headerName: 'Salary', aggFunc: 'sum' }
948
+ { field: 'salary', headerName: 'Salary', aggFunc: 'sum' },
905
949
  ];
906
-
950
+
907
951
  const pivotData: any[] = [
908
952
  { id: 1, name: 'John', dept: 'Engineering', location: 'NY', salary: 1000 },
909
953
  { id: 2, name: 'Jane', dept: 'Engineering', location: 'SF', salary: 2000 },
910
954
  { id: 3, name: 'Bob', dept: 'Sales', location: 'NY', salary: 1500 },
911
955
  { id: 4, name: 'Alice', dept: 'Sales', location: 'SF', salary: 2500 },
912
- { id: 5, name: 'Charlie', dept: 'Engineering', location: 'NY', salary: 1200 }
956
+ { id: 5, name: 'Charlie', dept: 'Engineering', location: 'NY', salary: 1200 },
913
957
  ];
914
958
 
915
959
  it('should generate pivot columns correctly', () => {
916
960
  const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: true });
917
961
  const columns = api.getAllColumns();
918
- const visibleColumns = columns.filter(c => c.visible);
919
-
962
+ const visibleColumns = columns.filter((c) => c.visible);
963
+
920
964
  // Auto Group + 2 pivot columns (NY, SF) = 3
921
965
  expect(visibleColumns.length).toBe(3);
922
- expect(visibleColumns.find(c => c.colId.includes('NY'))).toBeTruthy();
923
- expect(visibleColumns.find(c => c.colId.includes('SF'))).toBeTruthy();
966
+ expect(visibleColumns.find((c) => c.colId.includes('NY'))).toBeTruthy();
967
+ expect(visibleColumns.find((c) => c.colId.includes('SF'))).toBeTruthy();
924
968
  });
925
969
 
926
970
  it('should calculate pivoted values correctly', () => {
927
971
  const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: true });
928
-
972
+
929
973
  let engNode = null;
930
974
  for (let i = 0; i < api.getDisplayedRowCount(); i++) {
931
- const node = api.getDisplayedRowAtIndex(i);
932
- if (node?.group && node.data.dept === 'Engineering') {
933
- engNode = node;
934
- break;
935
- }
975
+ const node = api.getDisplayedRowAtIndex(i);
976
+ if (node?.group && node.data.dept === 'Engineering') {
977
+ engNode = node;
978
+ break;
979
+ }
936
980
  }
937
-
981
+
938
982
  expect(engNode).toBeTruthy();
939
- expect((engNode?.data as any).pivotData['NY'].salary).toBe(2200);
940
- expect((engNode?.data as any).pivotData['SF'].salary).toBe(2000);
983
+ expect((engNode?.data as any).pivotData.NY.salary).toBe(2200);
984
+ expect((engNode?.data as any).pivotData.SF.salary).toBe(2000);
941
985
  });
942
986
 
943
987
  it('should toggle pivot mode via API', () => {
944
988
  const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: false });
945
989
  expect(api.isPivotMode()).toBe(false);
946
-
990
+
947
991
  api.setPivotMode(true);
948
992
  expect(api.isPivotMode()).toBe(true);
949
- expect(api.getAllColumns().filter(c => c.visible).some(c => c.colId.startsWith('pivot_'))).toBe(true);
950
-
993
+ expect(
994
+ api
995
+ .getAllColumns()
996
+ .filter((c) => c.visible)
997
+ .some((c) => c.colId.startsWith('pivot_'))
998
+ ).toBe(true);
999
+
951
1000
  api.setPivotMode(false);
952
1001
  expect(api.isPivotMode()).toBe(false);
953
1002
  });
@@ -956,41 +1005,41 @@ describe('GridService', () => {
956
1005
  describe('Master/Detail', () => {
957
1006
  const mdColumnDefs: ColDef[] = [
958
1007
  { field: 'id', headerName: 'ID' },
959
- { field: 'name', headerName: 'Name' }
1008
+ { field: 'name', headerName: 'Name' },
960
1009
  ];
961
-
1010
+
962
1011
  const mdData: any[] = [
963
1012
  { id: 1, name: 'John' },
964
- { id: 2, name: 'Jane' }
1013
+ { id: 2, name: 'Jane' },
965
1014
  ];
966
1015
 
967
1016
  it('should identify master rows correctly', () => {
968
- const api = service.createApi(mdColumnDefs, mdData, {
1017
+ const api = service.createApi(mdColumnDefs, mdData, {
969
1018
  masterDetail: true,
970
- isRowMaster: (data) => data.id === 1
1019
+ isRowMaster: (data) => data.id === 1,
971
1020
  });
972
-
1021
+
973
1022
  const node1 = api.getRowNode('1');
974
1023
  const node2 = api.getRowNode('2');
975
-
1024
+
976
1025
  expect(node1?.master).toBe(true);
977
1026
  expect(node2?.master).toBe(false);
978
1027
  });
979
1028
 
980
1029
  it('should insert detail row when master is expanded', () => {
981
- const api = service.createApi(mdColumnDefs, mdData, {
1030
+ const api = service.createApi(mdColumnDefs, mdData, {
982
1031
  masterDetail: true,
983
- isRowMaster: (data) => data.id === 1
1032
+ isRowMaster: (data) => data.id === 1,
984
1033
  });
985
-
1034
+
986
1035
  expect(api.getDisplayedRowCount()).toBe(2);
987
-
1036
+
988
1037
  const node1 = api.getRowNode('1')!;
989
1038
  api.setRowNodeExpanded(node1, true);
990
-
1039
+
991
1040
  // Should now have 3 rows: Master 1, Detail 1, Master 2
992
1041
  expect(api.getDisplayedRowCount()).toBe(3);
993
-
1042
+
994
1043
  const detailNode = api.getDisplayedRowAtIndex(1);
995
1044
  expect(detailNode?.detail).toBe(true);
996
1045
  expect(detailNode?.id).toBe('1-detail');
@@ -998,42 +1047,957 @@ describe('GridService', () => {
998
1047
  });
999
1048
 
1000
1049
  it('should remove detail row when master is collapsed', () => {
1001
- const api = service.createApi(mdColumnDefs, mdData, {
1050
+ const api = service.createApi(mdColumnDefs, mdData, {
1002
1051
  masterDetail: true,
1003
- isRowMaster: (data) => data.id === 1
1052
+ isRowMaster: (data) => data.id === 1,
1004
1053
  });
1005
-
1054
+
1006
1055
  const node1 = api.getRowNode('1')!;
1007
1056
  api.setRowNodeExpanded(node1, true);
1008
1057
  expect(api.getDisplayedRowCount()).toBe(3);
1009
-
1058
+
1010
1059
  api.setRowNodeExpanded(node1, false);
1011
1060
  expect(api.getDisplayedRowCount()).toBe(2);
1012
1061
  });
1013
1062
 
1014
1063
  it('should calculate correct Y positions for variable heights', () => {
1015
- const api = service.createApi(mdColumnDefs, mdData, {
1064
+ const api = service.createApi(mdColumnDefs, mdData, {
1016
1065
  masterDetail: true,
1017
1066
  isRowMaster: (data) => data.id === 1,
1018
1067
  rowHeight: 30,
1019
- detailRowHeight: 100
1068
+ detailRowHeight: 100,
1020
1069
  });
1021
-
1070
+
1022
1071
  const node1 = api.getRowNode('1')!;
1023
1072
  api.setRowNodeExpanded(node1, true);
1024
-
1073
+
1025
1074
  // Row 0: Master (Y=0, H=30)
1026
1075
  // Row 1: Detail (Y=30, H=100)
1027
1076
  // Row 2: Master (Y=130, H=30)
1028
-
1077
+
1029
1078
  expect(api.getRowY(0)).toBe(0);
1030
1079
  expect(api.getRowY(1)).toBe(30);
1031
1080
  expect(api.getRowY(2)).toBe(130);
1032
1081
  expect(api.getTotalHeight()).toBe(160);
1033
-
1082
+
1034
1083
  expect(api.getRowAtY(15)).toBe(0);
1035
1084
  expect(api.getRowAtY(50)).toBe(1);
1036
1085
  expect(api.getRowAtY(140)).toBe(2);
1037
1086
  });
1038
1087
  });
1088
+
1089
+ describe('Export Functions', () => {
1090
+ it('should export as CSV with default params', () => {
1091
+ const exportApi = service.createApi(testColumnDefs, [...testRowData]);
1092
+ // Mock downloadFile
1093
+ const _originalDownload = (exportApi as any).downloadFile;
1094
+ (exportApi as any).downloadFile = vi.fn();
1095
+
1096
+ exportApi.exportDataAsCsv();
1097
+
1098
+ expect((exportApi as any).downloadFile).toHaveBeenCalled();
1099
+ const callArgs = (exportApi as any).downloadFile.mock.calls[0];
1100
+ expect(callArgs[2]).toBe('text/csv;charset=utf-8;');
1101
+ });
1102
+
1103
+ it('should export as CSV with custom params', () => {
1104
+ const exportApi = service.createApi(testColumnDefs, [...testRowData]);
1105
+ const _originalDownload = (exportApi as any).downloadFile;
1106
+ (exportApi as any).downloadFile = vi.fn();
1107
+
1108
+ exportApi.exportDataAsCsv({
1109
+ fileName: 'custom.csv',
1110
+ delimiter: ';',
1111
+ skipHeader: true,
1112
+ columnKeys: ['id', 'name'],
1113
+ });
1114
+
1115
+ expect((exportApi as any).downloadFile).toHaveBeenCalled();
1116
+ const callArgs = (exportApi as any).downloadFile.mock.calls[0];
1117
+ expect(callArgs[1]).toBe('custom.csv');
1118
+ });
1119
+
1120
+ it('should export as CSV with skipped columns', () => {
1121
+ const exportApi = service.createApi(testColumnDefs, [...testRowData]);
1122
+ const _originalDownload = (exportApi as any).downloadFile;
1123
+ (exportApi as any).downloadFile = vi.fn();
1124
+
1125
+ exportApi.exportDataAsCsv({
1126
+ columnKeys: ['email'],
1127
+ });
1128
+
1129
+ expect((exportApi as any).downloadFile).toHaveBeenCalled();
1130
+ });
1131
+
1132
+ it('should handle CSV special characters', () => {
1133
+ const specialData = [
1134
+ { id: 1, name: 'John, Jr.', age: 30, email: 'john@example.com' },
1135
+ { id: 2, name: 'Jane "The Boss"', age: 25, email: 'jane@example.com' },
1136
+ { id: 3, name: 'Bob\nJohnson', age: 35, email: 'bob@example.com' },
1137
+ ];
1138
+ const exportApi = service.createApi(testColumnDefs, specialData);
1139
+ const _originalDownload = (exportApi as any).downloadFile;
1140
+ (exportApi as any).downloadFile = vi.fn();
1141
+
1142
+ exportApi.exportDataAsCsv();
1143
+
1144
+ expect((exportApi as any).downloadFile).toHaveBeenCalled();
1145
+ const csvContent = (exportApi as any).downloadFile.mock.calls[0][0];
1146
+ expect(csvContent).toContain('"John, Jr."');
1147
+ expect(csvContent).toContain('"Jane ""The Boss"""');
1148
+ });
1149
+
1150
+ it('should export as Excel', () => {
1151
+ const exportApi = service.createApi(testColumnDefs, [...testRowData]);
1152
+ // Excel export is async, just verify it doesn't throw
1153
+ expect(() => exportApi.exportDataAsExcel()).not.toThrow();
1154
+ });
1155
+
1156
+ it('should export as Excel with custom params', () => {
1157
+ const exportApi = service.createApi(testColumnDefs, [...testRowData]);
1158
+ expect(() =>
1159
+ exportApi.exportDataAsExcel({
1160
+ fileName: 'custom.xlsx',
1161
+ sheetName: 'Data',
1162
+ skipHeader: true,
1163
+ columnKeys: ['id', 'name'],
1164
+ })
1165
+ ).not.toThrow();
1166
+ });
1167
+ });
1168
+
1169
+ describe('Column Operations', () => {
1170
+ it('should move column', () => {
1171
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1172
+ const columns = api.getAllColumns();
1173
+ expect(columns[0].colId).toBe('id');
1174
+
1175
+ api.moveColumn(columns[0], 2);
1176
+
1177
+ const movedColumns = api.getAllColumns();
1178
+ expect(movedColumns[2].colId).toBe('id');
1179
+ });
1180
+
1181
+ it('should set column width', () => {
1182
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1183
+ const column = api.getColumn('name');
1184
+
1185
+ api.setColumnWidth(column!, 200);
1186
+
1187
+ expect(column?.width).toBe(200);
1188
+ });
1189
+
1190
+ it('should set column pinned', () => {
1191
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1192
+ const column = api.getColumn('id');
1193
+
1194
+ api.setColumnPinned(column!, 'left');
1195
+
1196
+ expect(column?.pinned).toBe('left');
1197
+ });
1198
+
1199
+ it('should set column visible', () => {
1200
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1201
+ const column = api.getColumn('name');
1202
+
1203
+ api.setColumnVisible(column!, false);
1204
+
1205
+ expect(column?.visible).toBe(false);
1206
+ });
1207
+
1208
+ it('should set column sort', () => {
1209
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1210
+ const column = api.getColumn('age');
1211
+
1212
+ api.setColumnSort(column!, 'asc', false);
1213
+
1214
+ expect(column?.sort).toBe('asc');
1215
+ });
1216
+
1217
+ it('should auto-size columns', () => {
1218
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1219
+ const columns = api.getAllColumns();
1220
+
1221
+ api.autoSizeColumns(columns.map((c) => c.colId));
1222
+
1223
+ // Columns should have been resized
1224
+ expect(columns[0].width).toBeGreaterThan(0);
1225
+ });
1226
+
1227
+ it('should get column state', () => {
1228
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1229
+ const state = api.getColumnState();
1230
+
1231
+ expect(state).toBeDefined();
1232
+ expect(state.length).toBeGreaterThan(0);
1233
+ });
1234
+
1235
+ it('should apply column state', () => {
1236
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1237
+ const state = api.getColumnState();
1238
+
1239
+ // Modify state
1240
+ state[0].width = 300;
1241
+
1242
+ api.applyColumnState({ state, applyOrder: true });
1243
+
1244
+ expect(state[0].width).toBe(300);
1245
+ });
1246
+
1247
+ it('should reset column state', () => {
1248
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1249
+
1250
+ api.resetColumnState();
1251
+
1252
+ // Should not throw
1253
+ });
1254
+ });
1255
+
1256
+ describe('Clipboard Operations', () => {
1257
+ it('should copy to clipboard', () => {
1258
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1259
+ // Mock navigator.clipboard
1260
+ const originalClipboard = navigator.clipboard;
1261
+ Object.assign(navigator, {
1262
+ clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
1263
+ });
1264
+
1265
+ api.copyToClipboard();
1266
+
1267
+ expect(navigator.clipboard.writeText).toHaveBeenCalled();
1268
+
1269
+ // Restore
1270
+ Object.assign(navigator, { clipboard: originalClipboard });
1271
+ });
1272
+
1273
+ it('should paste from clipboard', () => {
1274
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1275
+ // Mock navigator.clipboard
1276
+ const originalClipboard = navigator.clipboard;
1277
+ Object.assign(navigator, {
1278
+ clipboard: { readText: vi.fn().mockResolvedValue('test\tpaste') },
1279
+ });
1280
+
1281
+ api.pasteFromClipboard();
1282
+
1283
+ expect(navigator.clipboard.readText).toHaveBeenCalled();
1284
+
1285
+ // Restore
1286
+ Object.assign(navigator, { clipboard: originalClipboard });
1287
+ });
1288
+ });
1289
+
1290
+ describe('Cell Editing', () => {
1291
+ it('should start editing cell', () => {
1292
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1293
+ api.startEditingCell({ rowIndex: 0, colKey: 'name' });
1294
+ // Should not throw
1295
+ });
1296
+
1297
+ it('should stop editing', () => {
1298
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1299
+ api.stopEditing();
1300
+ // Should not throw
1301
+ });
1302
+
1303
+ it('should get editing cells', () => {
1304
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1305
+ const cells = api.getEditingCells();
1306
+ expect(Array.isArray(cells)).toBe(true);
1307
+ });
1308
+
1309
+ it('should flash cells', () => {
1310
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1311
+ api.flashCells({
1312
+ rowNodes: [api.getDisplayedRowAtIndex(0)!],
1313
+ columns: ['name'],
1314
+ flashTime: 500,
1315
+ });
1316
+ // Should not throw
1317
+ });
1318
+ });
1319
+
1320
+ describe('Row Operations', () => {
1321
+ it('should refresh rows', () => {
1322
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1323
+ const node = api.getDisplayedRowAtIndex(0);
1324
+ api.refreshRows({ rowNodes: [node!] });
1325
+ // Should not throw
1326
+ });
1327
+
1328
+ it('should refresh cells', () => {
1329
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1330
+ api.refreshCells({
1331
+ rowNodes: [api.getDisplayedRowAtIndex(0)!],
1332
+ columns: ['name'],
1333
+ });
1334
+ // Should not throw
1335
+ });
1336
+
1337
+ it('should refresh header', () => {
1338
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1339
+ api.refreshHeader();
1340
+ // Should not throw
1341
+ });
1342
+
1343
+ it('should reset row heights', () => {
1344
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1345
+ api.resetRowHeights();
1346
+ // Should not throw
1347
+ });
1348
+
1349
+ it('should get row height for row', () => {
1350
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1351
+ const height = api.getRowHeightForRow(0);
1352
+ expect(height).toBe(32);
1353
+ });
1354
+ });
1355
+
1356
+ describe('Scroll Operations', () => {
1357
+ it('should set scroll position', () => {
1358
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1359
+ api.setScrollPosition({ top: 100, left: 50 });
1360
+ // Should not throw
1361
+ });
1362
+
1363
+ it('should get scroll position', () => {
1364
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1365
+ const pos = api.getScrollPosition();
1366
+ expect(pos).toBeDefined();
1367
+ });
1368
+
1369
+ it('should ensure column visible', () => {
1370
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1371
+ api.ensureColumnVisible('name');
1372
+ // Should not throw
1373
+ });
1374
+
1375
+ it('should ensure index visible', () => {
1376
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1377
+ api.ensureIndexVisible(10);
1378
+ // Should not throw
1379
+ });
1380
+
1381
+ it('should size columns to fit', () => {
1382
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1383
+ api.sizeColumnsToFit(800);
1384
+ // Should not throw
1385
+ });
1386
+ });
1387
+
1388
+ describe('Pivot Mode', () => {
1389
+ it('should set pivot mode', () => {
1390
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1391
+ api.setPivotMode(true);
1392
+ expect(api.isPivotMode()).toBe(true);
1393
+ });
1394
+
1395
+ it('should get pivot columns', () => {
1396
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1397
+ const pivotCols = api.getPivotColumns();
1398
+ expect(Array.isArray(pivotCols)).toBe(true);
1399
+ });
1400
+
1401
+ it('should get value columns', () => {
1402
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1403
+ const valueCols = api.getValueColumns();
1404
+ expect(Array.isArray(valueCols)).toBe(true);
1405
+ });
1406
+
1407
+ it('should get row group columns', () => {
1408
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1409
+ const groupCols = api.getRowGroupColumns();
1410
+ expect(Array.isArray(groupCols)).toBe(true);
1411
+ });
1412
+
1413
+ it('should get group display type', () => {
1414
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1415
+ const type = api.getGroupDisplayType();
1416
+ expect(type).toBe('singleColumn');
1417
+ });
1418
+ });
1419
+
1420
+ describe('Tool Panels', () => {
1421
+ it('should set side bar visible', () => {
1422
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1423
+ api.setSideBarVisible(true);
1424
+ // Should not throw
1425
+ });
1426
+
1427
+ it('should open tool panel', () => {
1428
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1429
+ api.openToolPanel('columns');
1430
+ // Should not throw
1431
+ });
1432
+
1433
+ it('should close tool panel', () => {
1434
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1435
+ api.closeToolPanel();
1436
+ // Should not throw
1437
+ });
1438
+
1439
+ it('should enable filter tool panel', () => {
1440
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1441
+ api.enableFilterToolPanel();
1442
+ // Should not throw
1443
+ });
1444
+
1445
+ it('should enable columns tool panel', () => {
1446
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1447
+ api.enableColumnsToolPanel();
1448
+ // Should not throw
1449
+ });
1450
+
1451
+ it('should get tool panel', () => {
1452
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1453
+ const _panel = api.getToolPanel('columns');
1454
+ // May be null if not initialized
1455
+ });
1456
+
1457
+ it('should check if tool panel is showing', () => {
1458
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1459
+ const showing = api.isToolPanelShowing();
1460
+ expect(typeof showing).toBe('boolean');
1461
+ });
1462
+ });
1463
+
1464
+ describe('Context Menu', () => {
1465
+ it('should get context menu items', () => {
1466
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1467
+ const items = api.getContextMenuItems();
1468
+ expect(Array.isArray(items)).toBe(true);
1469
+ });
1470
+
1471
+ it('should get main menu items', () => {
1472
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1473
+ const items = api.getMainMenuItems();
1474
+ expect(Array.isArray(items)).toBe(true);
1475
+ });
1476
+
1477
+ it('should get header context menu items', () => {
1478
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1479
+ const items = api.getHeaderContextMenuItems();
1480
+ expect(Array.isArray(items)).toBe(true);
1481
+ });
1482
+ });
1483
+
1484
+ describe('Focus Management', () => {
1485
+ it('should get focused cell', () => {
1486
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1487
+ const _cell = api.getFocusedCell();
1488
+ // May be null
1489
+ });
1490
+
1491
+ it('should set focused cell', () => {
1492
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1493
+ api.setFocusedCell({
1494
+ rowIndex: 0,
1495
+ colKey: 'name',
1496
+ rowPinned: null,
1497
+ forceBrowserFocus: false,
1498
+ });
1499
+ // Should not throw
1500
+ });
1501
+ });
1502
+
1503
+ describe('Event Handling', () => {
1504
+ it('should add event listener', () => {
1505
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1506
+ const listener = vi.fn();
1507
+ api.addEventListener('rowClicked', listener);
1508
+ // Should not throw
1509
+ });
1510
+
1511
+ it('should remove event listener', () => {
1512
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1513
+ const listener = vi.fn();
1514
+ api.addEventListener('rowClicked', listener);
1515
+ api.removeEventListener('rowClicked', listener);
1516
+ // Should not throw
1517
+ });
1518
+
1519
+ it('should dispatch event', () => {
1520
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1521
+ api.dispatchEvent('rowClicked', { data: {} });
1522
+ // Should not throw
1523
+ });
1524
+
1525
+ it('should get event path', () => {
1526
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1527
+ const path = api.getEventPath();
1528
+ expect(Array.isArray(path)).toBe(true);
1529
+ });
1530
+ });
1531
+
1532
+ describe('Rendering', () => {
1533
+ it('should get rendered nodes', () => {
1534
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1535
+ const nodes = api.getRenderedNodes();
1536
+ expect(Array.isArray(nodes)).toBe(true);
1537
+ });
1538
+
1539
+ it('should get first rendered row', () => {
1540
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1541
+ const row = api.getFirstRenderedRow();
1542
+ expect(typeof row).toBe('number');
1543
+ });
1544
+
1545
+ it('should get last rendered row', () => {
1546
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1547
+ const row = api.getLastRenderedRow();
1548
+ expect(typeof row).toBe('number');
1549
+ });
1550
+
1551
+ it('should get vertical pixel range', () => {
1552
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1553
+ const range = api.getVerticalPixelRange();
1554
+ expect(range).toBeDefined();
1555
+ });
1556
+
1557
+ it('should get horizontal pixel range', () => {
1558
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1559
+ const range = api.getHorizontalPixelRange();
1560
+ expect(range).toBeDefined();
1561
+ });
1562
+
1563
+ it('should get pinned width', () => {
1564
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1565
+ const width = api.getPinnedWidth();
1566
+ expect(typeof width).toBe('number');
1567
+ });
1568
+
1569
+ it('should get right pinned width', () => {
1570
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1571
+ const width = api.getRightPinnedWidth();
1572
+ expect(typeof width).toBe('number');
1573
+ });
1574
+
1575
+ it('should get H scroll position', () => {
1576
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1577
+ const pos = api.getHScrollPosition();
1578
+ expect(typeof pos).toBe('number');
1579
+ });
1580
+
1581
+ it('should get V scroll position', () => {
1582
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1583
+ const pos = api.getVScrollPosition();
1584
+ expect(typeof pos).toBe('number');
1585
+ });
1586
+ });
1587
+
1588
+ describe('Localization', () => {
1589
+ it('should get locale text', () => {
1590
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1591
+ const text = api.getLocaleText();
1592
+ expect(typeof text).toBe('string');
1593
+ });
1594
+
1595
+ it('should set locale text', () => {
1596
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1597
+ api.setLocaleText('en', { apply: 'Apply' });
1598
+ // Should not throw
1599
+ });
1600
+ });
1601
+
1602
+ describe('Charts', () => {
1603
+ it('should get chart models', () => {
1604
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1605
+ const models = api.getChartModels();
1606
+ expect(Array.isArray(models)).toBe(true);
1607
+ });
1608
+
1609
+ it('should get chart toolbar items', () => {
1610
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1611
+ const items = api.getChartToolbarItems();
1612
+ expect(Array.isArray(items)).toBe(true);
1613
+ });
1614
+
1615
+ it('should hide popup', () => {
1616
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1617
+ api.hidePopup();
1618
+ // Should not throw
1619
+ });
1620
+
1621
+ it('should get sparkline options', () => {
1622
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1623
+ const options = api.getSparklineOptions();
1624
+ expect(Array.isArray(options)).toBe(true);
1625
+ });
1626
+ });
1627
+
1628
+ describe('Grid State', () => {
1629
+ it('should get grid panel', () => {
1630
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1631
+ const _panel = api.getGridPanel();
1632
+ // May be null
1633
+ });
1634
+
1635
+ it('should get row container element', () => {
1636
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1637
+ const _element = api.getRowContainerElement();
1638
+ // May be null
1639
+ });
1640
+
1641
+ it('should get body element', () => {
1642
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1643
+ const _element = api.getBodyElement();
1644
+ // May be null
1645
+ });
1646
+
1647
+ it('should get header elements', () => {
1648
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1649
+ const elements = api.getHeaderElements();
1650
+ expect(Array.isArray(elements)).toBe(true);
1651
+ });
1652
+
1653
+ it('should get center elements', () => {
1654
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1655
+ const elements = api.getCenterElements();
1656
+ expect(Array.isArray(elements)).toBe(true);
1657
+ });
1658
+
1659
+ it('should get left elements', () => {
1660
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1661
+ const elements = api.getLeftElements();
1662
+ expect(Array.isArray(elements)).toBe(true);
1663
+ });
1664
+
1665
+ it('should get right elements', () => {
1666
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1667
+ const elements = api.getRightElements();
1668
+ expect(Array.isArray(elements)).toBe(true);
1669
+ });
1670
+ });
1671
+
1672
+ describe('Disabled State', () => {
1673
+ it('should set disabled', () => {
1674
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1675
+ api.setDisabled(true);
1676
+ // Should not throw
1677
+ });
1678
+
1679
+ it('should check if disabled', () => {
1680
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1681
+ const disabled = api.isDisabled();
1682
+ expect(typeof disabled).toBe('boolean');
1683
+ });
1684
+ });
1685
+
1686
+ describe('Row Information', () => {
1687
+ it('should get row position', () => {
1688
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1689
+ const pos = api.getRowPosition(0);
1690
+ expect(typeof pos).toBe('number');
1691
+ });
1692
+
1693
+ it('should get row style', () => {
1694
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1695
+ const _style = api.getRowStyle(0);
1696
+ // May be null
1697
+ });
1698
+
1699
+ it('should get row class', () => {
1700
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1701
+ const _cls = api.getRowClass(0);
1702
+ // May be null
1703
+ });
1704
+
1705
+ it('should get row id', () => {
1706
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1707
+ const node = api.getDisplayedRowAtIndex(0);
1708
+ const id = api.getRowId(node!);
1709
+ expect(id).toBe('1'); // First row has id: 1 in test data
1710
+ });
1711
+
1712
+ it('should check if row is master', () => {
1713
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1714
+ const node = api.getDisplayedRowAtIndex(0);
1715
+ const isMaster = api.isRowMaster(node!);
1716
+ expect(typeof isMaster).toBe('boolean');
1717
+ });
1718
+
1719
+ it('should get row group columns', () => {
1720
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1721
+ const cols = api.getRowGroupColumns();
1722
+ expect(Array.isArray(cols)).toBe(true);
1723
+ });
1724
+
1725
+ it('should get column groups', () => {
1726
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1727
+ const groups = api.getColumnGroups();
1728
+ expect(Array.isArray(groups)).toBe(true);
1729
+ });
1730
+
1731
+ it('should get column group', () => {
1732
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1733
+ const _group = api.getColumnGroup();
1734
+ // May be null
1735
+ });
1736
+ });
1737
+
1738
+ describe('Aggregation', () => {
1739
+ it('should refresh aggregated cols', () => {
1740
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1741
+ api.refreshAggregatedCols();
1742
+ // Should not throw
1743
+ });
1744
+ });
1745
+
1746
+ describe('ForEach Operations', () => {
1747
+ it('should forEach node', () => {
1748
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1749
+ const callback = vi.fn();
1750
+ api.forEachNode(callback);
1751
+ expect(callback).toHaveBeenCalled();
1752
+ });
1753
+
1754
+ it('should forEach node after filter', () => {
1755
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1756
+ const callback = vi.fn();
1757
+ api.forEachNodeAfterFilter(callback);
1758
+ expect(callback).toHaveBeenCalled();
1759
+ });
1760
+
1761
+ it('should forEach node after filter and sort', () => {
1762
+ const api = service.createApi(testColumnDefs, [...testRowData]);
1763
+ const callback = vi.fn();
1764
+ api.forEachNodeAfterFilterAndSort(callback);
1765
+ expect(callback).toHaveBeenCalled();
1766
+ });
1767
+ });
1768
+
1769
+ // ============================================================================
1770
+ // STATE PERSISTENCE TESTS
1771
+ // ============================================================================
1772
+
1773
+ describe('State Persistence', () => {
1774
+ beforeEach(() => {
1775
+ // Clear localStorage before each test
1776
+ localStorage.clear();
1777
+ });
1778
+
1779
+ describe('getState', () => {
1780
+ it('should return grid state object', () => {
1781
+ service.createApi(testColumnDefs, [...testRowData]);
1782
+ const state = service.getState();
1783
+
1784
+ expect(state).toBeDefined();
1785
+ expect(state.columnOrder).toBeDefined();
1786
+ expect(state.filter).toBeDefined();
1787
+ expect(state.sort).toBeDefined();
1788
+ });
1789
+
1790
+ it('should include column order, width, and visibility', () => {
1791
+ service.createApi(testColumnDefs, [...testRowData]);
1792
+ const state = service.getState();
1793
+
1794
+ expect(state.columnOrder).toHaveLength(4);
1795
+ expect(state.columnOrder?.[0]).toHaveProperty('colId');
1796
+ expect(state.columnOrder?.[0]).toHaveProperty('width');
1797
+ expect(state.columnOrder?.[0]).toHaveProperty('hide');
1798
+ });
1799
+
1800
+ it('should include filter model', () => {
1801
+ service.createApi(testColumnDefs, [...testRowData]);
1802
+ const state = service.getState();
1803
+
1804
+ expect(state.filter).toEqual({});
1805
+ });
1806
+
1807
+ it('should include sort model', () => {
1808
+ service.createApi(testColumnDefs, [...testRowData]);
1809
+ const state = service.getState();
1810
+
1811
+ expect(state.sort).toEqual({ sortModel: [] });
1812
+ });
1813
+ });
1814
+
1815
+ describe('setState', () => {
1816
+ it('should restore column state', () => {
1817
+ service.createApi(testColumnDefs, [...testRowData]);
1818
+ const state: GridState = {
1819
+ columnOrder: [
1820
+ { colId: 'id', width: 150, hide: false, pinned: false },
1821
+ { colId: 'name', width: 200, hide: true, pinned: 'left' },
1822
+ ],
1823
+ };
1824
+
1825
+ expect(() => service.setState(state)).not.toThrow();
1826
+ });
1827
+
1828
+ it('should restore filter state', () => {
1829
+ service.createApi(testColumnDefs, [...testRowData]);
1830
+ const state: GridState = {
1831
+ filter: { id: { filterType: 'number', type: 'greaterThan', filter: 1 } },
1832
+ };
1833
+
1834
+ expect(() => service.setState(state)).not.toThrow();
1835
+ });
1836
+
1837
+ it('should restore sort state', () => {
1838
+ service.createApi(testColumnDefs, [...testRowData]);
1839
+ const state: GridState = {
1840
+ sort: [{ colId: 'name', sort: 'asc' }],
1841
+ };
1842
+
1843
+ expect(() => service.setState(state)).not.toThrow();
1844
+ });
1845
+
1846
+ it('should emit state change event', () => {
1847
+ service.createApi(testColumnDefs, [...testRowData]);
1848
+ const stateChangeSpy = vi.fn();
1849
+ service.gridStateChanged$.subscribe(stateChangeSpy);
1850
+
1851
+ service.setState({ columnOrder: [] });
1852
+
1853
+ expect(stateChangeSpy).toHaveBeenCalledWith({ type: 'state-restored' });
1854
+ });
1855
+ });
1856
+
1857
+ describe('saveState', () => {
1858
+ it('should save state to localStorage', () => {
1859
+ service.createApi(testColumnDefs, [...testRowData]);
1860
+ service.saveState('test-key');
1861
+
1862
+ const saved = localStorage.getItem('test-key');
1863
+ expect(saved).not.toBeNull();
1864
+ expect(() => JSON.parse(saved!)).not.toThrow();
1865
+ });
1866
+
1867
+ it('should use default key if not provided', () => {
1868
+ service.createApi(testColumnDefs, [...testRowData]);
1869
+ service.saveState();
1870
+
1871
+ const saved = localStorage.getItem('argent-grid-state');
1872
+ expect(saved).not.toBeNull();
1873
+ });
1874
+
1875
+ it('should emit state saved event', () => {
1876
+ service.createApi(testColumnDefs, [...testRowData]);
1877
+ const stateChangeSpy = vi.fn();
1878
+ service.gridStateChanged$.subscribe(stateChangeSpy);
1879
+
1880
+ service.saveState('test-key');
1881
+
1882
+ expect(stateChangeSpy).toHaveBeenCalledWith({ type: 'state-saved', key: 'test-key' });
1883
+ });
1884
+
1885
+ it('should handle errors gracefully without throwing', () => {
1886
+ service.createApi(testColumnDefs, [...testRowData]);
1887
+
1888
+ // Just verify the method doesn't throw even if localStorage fails
1889
+ // (We can't easily mock localStorage in jsdom environment)
1890
+ expect(() => service.saveState('test-key')).not.toThrow();
1891
+ });
1892
+ });
1893
+
1894
+ describe('restoreState', () => {
1895
+ it('should restore state from localStorage', () => {
1896
+ service.createApi(testColumnDefs, [...testRowData]);
1897
+ const state: GridState = {
1898
+ columnOrder: [{ colId: 'id', width: 150, hide: false, pinned: false }],
1899
+ };
1900
+ localStorage.setItem('test-key', JSON.stringify(state));
1901
+
1902
+ const result = service.restoreState('test-key');
1903
+
1904
+ expect(result).toBe(true);
1905
+ });
1906
+
1907
+ it('should return false if no state exists', () => {
1908
+ service.createApi(testColumnDefs, [...testRowData]);
1909
+ const result = service.restoreState('non-existent-key');
1910
+
1911
+ expect(result).toBe(false);
1912
+ });
1913
+
1914
+ it('should use default key if not provided', () => {
1915
+ service.createApi(testColumnDefs, [...testRowData]);
1916
+ const state: GridState = { columnOrder: [] };
1917
+ localStorage.setItem('argent-grid-state', JSON.stringify(state));
1918
+
1919
+ const result = service.restoreState();
1920
+
1921
+ expect(result).toBe(true);
1922
+ });
1923
+
1924
+ it('should emit state restored event', () => {
1925
+ service.createApi(testColumnDefs, [...testRowData]);
1926
+ const state: GridState = { columnOrder: [] };
1927
+ localStorage.setItem('test-key', JSON.stringify(state));
1928
+
1929
+ const stateChangeSpy = vi.fn();
1930
+ service.gridStateChanged$.subscribe(stateChangeSpy);
1931
+
1932
+ service.restoreState('test-key');
1933
+
1934
+ expect(stateChangeSpy).toHaveBeenCalledWith({ type: 'state-restored', key: 'test-key' });
1935
+ });
1936
+
1937
+ it('should handle invalid JSON gracefully', () => {
1938
+ service.createApi(testColumnDefs, [...testRowData]);
1939
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1940
+
1941
+ localStorage.setItem('test-key', 'invalid-json');
1942
+ const result = service.restoreState('test-key');
1943
+
1944
+ expect(result).toBe(false);
1945
+ expect(consoleSpy).toHaveBeenCalled();
1946
+
1947
+ consoleSpy.mockRestore();
1948
+ });
1949
+ });
1950
+
1951
+ describe('clearState', () => {
1952
+ it('should remove state from localStorage', () => {
1953
+ service.createApi(testColumnDefs, [...testRowData]);
1954
+ localStorage.setItem('test-key', JSON.stringify({ columnOrder: [] }));
1955
+
1956
+ service.clearState('test-key');
1957
+
1958
+ expect(localStorage.getItem('test-key')).toBeNull();
1959
+ });
1960
+
1961
+ it('should use default key if not provided', () => {
1962
+ service.createApi(testColumnDefs, [...testRowData]);
1963
+ localStorage.setItem('argent-grid-state', JSON.stringify({ columnOrder: [] }));
1964
+
1965
+ service.clearState();
1966
+
1967
+ expect(localStorage.getItem('argent-grid-state')).toBeNull();
1968
+ });
1969
+
1970
+ it('should emit state cleared event', () => {
1971
+ service.createApi(testColumnDefs, [...testRowData]);
1972
+ const stateChangeSpy = vi.fn();
1973
+ service.gridStateChanged$.subscribe(stateChangeSpy);
1974
+
1975
+ service.clearState('test-key');
1976
+
1977
+ expect(stateChangeSpy).toHaveBeenCalledWith({ type: 'state-cleared', key: 'test-key' });
1978
+ });
1979
+ });
1980
+
1981
+ describe('hasState', () => {
1982
+ it('should return true if state exists', () => {
1983
+ service.createApi(testColumnDefs, [...testRowData]);
1984
+ localStorage.setItem('test-key', JSON.stringify({ columnOrder: [] }));
1985
+
1986
+ expect(service.hasState('test-key')).toBe(true);
1987
+ });
1988
+
1989
+ it('should return false if state does not exist', () => {
1990
+ service.createApi(testColumnDefs, [...testRowData]);
1991
+
1992
+ expect(service.hasState('non-existent-key')).toBe(false);
1993
+ });
1994
+
1995
+ it('should use default key if not provided', () => {
1996
+ service.createApi(testColumnDefs, [...testRowData]);
1997
+ localStorage.setItem('argent-grid-state', JSON.stringify({ columnOrder: [] }));
1998
+
1999
+ expect(service.hasState()).toBe(true);
2000
+ });
2001
+ });
2002
+ });
1039
2003
  });