argent-grid 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pages.yml +68 -0
- package/AGENTS.md +179 -0
- package/README.md +222 -0
- package/demo-app/README.md +70 -0
- package/demo-app/angular.json +78 -0
- package/demo-app/e2e/benchmark.spec.ts +53 -0
- package/demo-app/e2e/demo-page.spec.ts +77 -0
- package/demo-app/e2e/grid-features.spec.ts +269 -0
- package/demo-app/package-lock.json +14023 -0
- package/demo-app/package.json +36 -0
- package/demo-app/playwright-test-menu.js +19 -0
- package/demo-app/playwright.config.ts +23 -0
- package/demo-app/src/app/app.component.ts +10 -0
- package/demo-app/src/app/app.config.ts +13 -0
- package/demo-app/src/app/app.routes.ts +7 -0
- package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
- package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
- package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
- package/demo-app/src/index.html +19 -0
- package/demo-app/src/main.ts +6 -0
- package/demo-app/tsconfig.json +31 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/plan.md +131 -0
- package/setup-vitest.ts +18 -0
- package/src/lib/argent-grid.module.ts +21 -0
- package/src/lib/components/argent-grid.component.css +483 -0
- package/src/lib/components/argent-grid.component.html +320 -0
- package/src/lib/components/argent-grid.component.spec.ts +189 -0
- package/src/lib/components/argent-grid.component.ts +1188 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
- package/src/lib/rendering/canvas-renderer.ts +962 -0
- package/src/lib/rendering/render/blit.spec.ts +453 -0
- package/src/lib/rendering/render/blit.ts +393 -0
- package/src/lib/rendering/render/cells.ts +369 -0
- package/src/lib/rendering/render/index.ts +105 -0
- package/src/lib/rendering/render/lines.ts +363 -0
- package/src/lib/rendering/render/theme.spec.ts +282 -0
- package/src/lib/rendering/render/theme.ts +201 -0
- package/src/lib/rendering/render/types.ts +279 -0
- package/src/lib/rendering/render/walk.spec.ts +360 -0
- package/src/lib/rendering/render/walk.ts +360 -0
- package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
- package/src/lib/rendering/utils/damage-tracker.ts +423 -0
- package/src/lib/rendering/utils/index.ts +7 -0
- package/src/lib/services/grid.service.spec.ts +1039 -0
- package/src/lib/services/grid.service.ts +1284 -0
- package/src/lib/types/ag-grid-types.ts +970 -0
- package/src/public-api.ts +22 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +55 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
|
3
|
+
import { GridService } from './grid.service';
|
|
4
|
+
import { GridApi, ColDef, FilterModel, IRowNode } from '../types/ag-grid-types';
|
|
5
|
+
|
|
6
|
+
interface TestData {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
age: number;
|
|
10
|
+
email: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('GridService', () => {
|
|
14
|
+
let service: GridService<TestData>;
|
|
15
|
+
let api: GridApi<TestData>;
|
|
16
|
+
|
|
17
|
+
const testColumnDefs: (ColDef<TestData>)[] = [
|
|
18
|
+
{ colId: 'id', field: 'id', headerName: 'ID', width: 100 },
|
|
19
|
+
{ colId: 'name', field: 'name', headerName: 'Name', width: 150 },
|
|
20
|
+
{ colId: 'age', field: 'age', headerName: 'Age', width: 80, sortable: true },
|
|
21
|
+
{ colId: 'email', field: 'email', headerName: 'Email', width: 200 }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const testRowData: TestData[] = [
|
|
25
|
+
{ id: 1, name: 'John Doe', age: 30, email: 'john@example.com' },
|
|
26
|
+
{ id: 2, name: 'Jane Smith', age: 25, email: 'jane@example.com' },
|
|
27
|
+
{ id: 3, name: 'Bob Johnson', age: 35, email: 'bob@example.com' }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
TestBed.configureTestingModule({
|
|
32
|
+
providers: [
|
|
33
|
+
GridService,
|
|
34
|
+
provideExperimentalZonelessChangeDetection()
|
|
35
|
+
]
|
|
36
|
+
});
|
|
37
|
+
service = TestBed.inject(GridService);
|
|
38
|
+
api = service.createApi(testColumnDefs, [...testRowData]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should be created', () => {
|
|
42
|
+
expect(service).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should initialize with correct row data', () => {
|
|
46
|
+
const data = api.getRowData();
|
|
47
|
+
expect(data).toEqual(testRowData);
|
|
48
|
+
expect(data.length).toBe(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should initialize columns correctly', () => {
|
|
52
|
+
const columns = api.getAllColumns();
|
|
53
|
+
expect(columns.length).toBe(4);
|
|
54
|
+
expect(columns[0].colId).toBe('id');
|
|
55
|
+
expect(columns[0].headerName).toBe('ID');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should get row node by id', () => {
|
|
59
|
+
const node = api.getRowNode('1');
|
|
60
|
+
expect(node).toBeTruthy();
|
|
61
|
+
expect(node?.data.id).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle row selection', () => {
|
|
65
|
+
const selectionApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
66
|
+
selectionApi.selectAll();
|
|
67
|
+
expect(selectionApi.getSelectedRows().length).toBe(3);
|
|
68
|
+
|
|
69
|
+
selectionApi.deselectAll();
|
|
70
|
+
expect(selectionApi.getSelectedRows().length).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should apply sorting', () => {
|
|
74
|
+
const sortApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
75
|
+
// Sort by age descending
|
|
76
|
+
sortApi.setSortModel([{ colId: 'age', sort: 'desc' }]);
|
|
77
|
+
|
|
78
|
+
const sortedData = sortApi.getRowData();
|
|
79
|
+
expect(sortedData[0].age).toBe(35);
|
|
80
|
+
expect(sortedData[2].age).toBe(25);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should apply sorting ascending', () => {
|
|
84
|
+
const sortApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
85
|
+
// Sort by age ascending
|
|
86
|
+
sortApi.setSortModel([{ colId: 'age', sort: 'asc' }]);
|
|
87
|
+
|
|
88
|
+
const sortedData = sortApi.getRowData();
|
|
89
|
+
expect(sortedData[0].age).toBe(25);
|
|
90
|
+
expect(sortedData[2].age).toBe(35);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle transaction - add rows and respect sorting', () => {
|
|
94
|
+
const sortApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
95
|
+
sortApi.setSortModel([{ colId: 'name', sort: 'asc' }]);
|
|
96
|
+
|
|
97
|
+
// Initial alpha: Bob Johnson (35), Jane Smith (25), John Doe (30)
|
|
98
|
+
expect(sortApi.getDisplayedRowAtIndex(0)?.data.name).toBe('Bob Johnson');
|
|
99
|
+
|
|
100
|
+
sortApi.applyTransaction({
|
|
101
|
+
add: [{ id: 4, name: 'Alice', age: 28, email: 'alice@example.com' }]
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Alice should now be first
|
|
105
|
+
expect(sortApi.getDisplayedRowAtIndex(0)?.data.name).toBe('Alice');
|
|
106
|
+
expect(sortApi.getDisplayedRowCount()).toBe(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle transaction - update rows and respect filtering', () => {
|
|
110
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
111
|
+
filterApi.setFilterModel({
|
|
112
|
+
age: { filterType: 'number', type: 'greaterThan', filter: 30 }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(filterApi.getDisplayedRowCount()).toBe(1); // Only Bob (35)
|
|
116
|
+
|
|
117
|
+
// Update Jane (25) to be 40
|
|
118
|
+
filterApi.applyTransaction({
|
|
119
|
+
update: [{ id: 2, name: 'Jane Smith', age: 40, email: 'jane@example.com' }]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(filterApi.getDisplayedRowCount()).toBe(2); // Bob and Jane
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle transaction - remove rows', () => {
|
|
126
|
+
const initialCount = api.getDisplayedRowCount();
|
|
127
|
+
const firstNode = api.getDisplayedRowAtIndex(0);
|
|
128
|
+
if (!firstNode || !firstNode.id) return;
|
|
129
|
+
|
|
130
|
+
const removeData: TestData = { id: firstNode.data.id, name: firstNode.data.name, age: firstNode.data.age, email: firstNode.data.email };
|
|
131
|
+
const result = api.applyTransaction({
|
|
132
|
+
remove: [removeData]
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result?.remove.length).toBe(1);
|
|
136
|
+
expect(api.getDisplayedRowCount()).toBe(initialCount - 1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should get and set filter model', () => {
|
|
140
|
+
const filterModel: FilterModel = {
|
|
141
|
+
name: { filterType: 'text', type: 'contains', filter: 'John' }
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
api.setFilterModel(filterModel);
|
|
145
|
+
expect(api.getFilterModel()).toEqual(filterModel);
|
|
146
|
+
expect(api.isFilterPresent()).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should apply text filter - contains', () => {
|
|
150
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
151
|
+
filterApi.setFilterModel({
|
|
152
|
+
name: { filterType: 'text', type: 'contains', filter: 'John' }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const data = filterApi.getRowData();
|
|
156
|
+
// Filter should match 'John Doe' and 'Bob Johnson'
|
|
157
|
+
expect(data.length).toBeLessThan(3);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should apply text filter - starts with', () => {
|
|
161
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
162
|
+
filterApi.setFilterModel({
|
|
163
|
+
name: { filterType: 'text', type: 'startsWith', filter: 'J' }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const data = filterApi.getRowData();
|
|
167
|
+
// Should match 'John Doe' and 'Jane Smith'
|
|
168
|
+
data.forEach(row => {
|
|
169
|
+
expect(row.name.startsWith('J')).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should apply text filter - ends with', () => {
|
|
174
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
175
|
+
filterApi.setFilterModel({
|
|
176
|
+
name: { filterType: 'text', type: 'endsWith', filter: 'e' }
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const data = filterApi.getRowData();
|
|
180
|
+
data.forEach(row => {
|
|
181
|
+
expect(row.name.endsWith('e')).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should apply text filter - equals', () => {
|
|
186
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
187
|
+
filterApi.setFilterModel({
|
|
188
|
+
name: { filterType: 'text', type: 'equals', filter: 'John Doe' }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const data = filterApi.getRowData();
|
|
192
|
+
expect(data.length).toBe(1);
|
|
193
|
+
expect(data[0].name).toBe('John Doe');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should apply number filter - greater than', () => {
|
|
197
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
198
|
+
filterApi.setFilterModel({
|
|
199
|
+
age: { filterType: 'number', type: 'greaterThan', filter: 28 }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const data = filterApi.getRowData();
|
|
203
|
+
data.forEach(row => {
|
|
204
|
+
expect(row.age).toBeGreaterThan(28);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should apply number filter - less than', () => {
|
|
209
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
210
|
+
filterApi.setFilterModel({
|
|
211
|
+
age: { filterType: 'number', type: 'lessThan', filter: 32 }
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const data = filterApi.getRowData();
|
|
215
|
+
data.forEach(row => {
|
|
216
|
+
expect(row.age).toBeLessThan(32);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should apply number filter - in range', () => {
|
|
221
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
222
|
+
filterApi.setFilterModel({
|
|
223
|
+
age: { filterType: 'number', type: 'inRange', filter: 26, filterTo: 34 }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const data = filterApi.getRowData();
|
|
227
|
+
data.forEach(row => {
|
|
228
|
+
expect(row.age).toBeGreaterThanOrEqual(26);
|
|
229
|
+
expect(row.age).toBeLessThanOrEqual(34);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should apply date filter', () => {
|
|
234
|
+
const dateData: any[] = [
|
|
235
|
+
{ id: 1, name: 'Event 1', date: '2024-01-15' },
|
|
236
|
+
{ id: 2, name: 'Event 2', date: '2024-06-20' },
|
|
237
|
+
{ id: 3, name: 'Event 3', date: '2024-12-01' }
|
|
238
|
+
];
|
|
239
|
+
const dateColumnDefs: any[] = [
|
|
240
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
241
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
242
|
+
{ colId: 'date', field: 'date', headerName: 'Date', filter: 'agDateColumnFilter' }
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const filterApi = service.createApi(dateColumnDefs, dateData);
|
|
246
|
+
filterApi.setFilterModel({
|
|
247
|
+
date: { filterType: 'date', type: 'greaterThan', filter: '2024-03-01' }
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const data = filterApi.getRowData();
|
|
251
|
+
// Should match events after March 2024
|
|
252
|
+
expect(data.length).toBe(2);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should clear filter when model is empty', () => {
|
|
256
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
257
|
+
filterApi.setFilterModel({
|
|
258
|
+
name: { filterType: 'text', type: 'contains', filter: 'John' }
|
|
259
|
+
});
|
|
260
|
+
expect(api.isFilterPresent()).toBe(true);
|
|
261
|
+
|
|
262
|
+
filterApi.setFilterModel({});
|
|
263
|
+
expect(filterApi.isFilterPresent()).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should combine multiple filters (AND logic)', () => {
|
|
267
|
+
const filterApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
268
|
+
filterApi.setFilterModel({
|
|
269
|
+
name: { filterType: 'text', type: 'startsWith', filter: 'J' },
|
|
270
|
+
age: { filterType: 'number', type: 'lessThan', filter: 30 }
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const data = filterApi.getRowData();
|
|
274
|
+
// Should match 'Jane Smith' (starts with J and age < 30)
|
|
275
|
+
data.forEach(row => {
|
|
276
|
+
expect(row.name.startsWith('J')).toBe(true);
|
|
277
|
+
expect(row.age).toBeLessThan(30);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Row Grouping Tests
|
|
282
|
+
it('should group rows by column', () => {
|
|
283
|
+
const groupData: any[] = [
|
|
284
|
+
{ id: 1, name: 'John', department: 'Engineering', salary: 80000 },
|
|
285
|
+
{ id: 2, name: 'Jane', department: 'Engineering', salary: 90000 },
|
|
286
|
+
{ id: 3, name: 'Bob', department: 'Sales', salary: 70000 },
|
|
287
|
+
{ id: 4, name: 'Alice', department: 'Sales', salary: 75000 }
|
|
288
|
+
];
|
|
289
|
+
const groupColumnDefs: ColDef[] = [
|
|
290
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
291
|
+
{ colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
|
|
292
|
+
{ colId: 'salary', field: 'salary', headerName: 'Salary' }
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const groupApi = service.createApi(groupColumnDefs, groupData);
|
|
296
|
+
|
|
297
|
+
// With groups collapsed, should show 2 group rows
|
|
298
|
+
const displayedCount = groupApi.getDisplayedRowCount();
|
|
299
|
+
expect(displayedCount).toBe(2); // 2 groups (Engineering, Sales)
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should expand and collapse groups', () => {
|
|
303
|
+
const groupData: any[] = [
|
|
304
|
+
{ id: 1, name: 'John', department: 'Engineering' },
|
|
305
|
+
{ id: 2, name: 'Jane', department: 'Engineering' },
|
|
306
|
+
{ id: 3, name: 'Bob', department: 'Sales' }
|
|
307
|
+
];
|
|
308
|
+
const groupColumnDefs: ColDef[] = [
|
|
309
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
310
|
+
{ colId: 'department', field: 'department', headerName: 'Department', rowGroup: true }
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
const groupApi = service.createApi(groupColumnDefs, groupData);
|
|
314
|
+
|
|
315
|
+
// Initially groups are collapsed
|
|
316
|
+
let displayedCount = groupApi.getDisplayedRowCount();
|
|
317
|
+
expect(displayedCount).toBe(2); // 2 groups
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should calculate group aggregations', () => {
|
|
321
|
+
const groupData: any[] = [
|
|
322
|
+
{ id: 1, name: 'John', department: 'Engineering', salary: 80000 },
|
|
323
|
+
{ id: 2, name: 'Jane', department: 'Engineering', salary: 90000 },
|
|
324
|
+
{ id: 3, name: 'Bob', department: 'Sales', salary: 70000 }
|
|
325
|
+
];
|
|
326
|
+
const groupColumnDefs: ColDef[] = [
|
|
327
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
328
|
+
{ colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
|
|
329
|
+
{ colId: 'salary', field: 'salary', headerName: 'Salary', aggFunc: 'sum' }
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const groupApi = service.createApi(groupColumnDefs, groupData);
|
|
333
|
+
|
|
334
|
+
// Verify grouping works
|
|
335
|
+
const displayedCount = groupApi.getDisplayedRowCount();
|
|
336
|
+
expect(displayedCount).toBeGreaterThanOrEqual(2); // At least 2 groups
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should support multiple row group columns', () => {
|
|
340
|
+
const groupData: any[] = [
|
|
341
|
+
{ id: 1, name: 'John', department: 'Engineering', level: 'Senior' },
|
|
342
|
+
{ id: 2, name: 'Jane', department: 'Engineering', level: 'Junior' },
|
|
343
|
+
{ id: 3, name: 'Bob', department: 'Sales', level: 'Senior' }
|
|
344
|
+
];
|
|
345
|
+
const groupColumnDefs: ColDef[] = [
|
|
346
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
347
|
+
{ colId: 'department', field: 'department', headerName: 'Department', rowGroup: true },
|
|
348
|
+
{ colId: 'level', field: 'level', headerName: 'Level', rowGroup: true }
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
const groupApi = service.createApi(groupColumnDefs, groupData);
|
|
352
|
+
|
|
353
|
+
// Should have hierarchical groups (Engineering/Senior, Engineering/Junior, Sales/Senior)
|
|
354
|
+
const displayedCount = groupApi.getDisplayedRowCount();
|
|
355
|
+
expect(displayedCount).toBe(2); // Engineering and Sales top level
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should get grid state', () => {
|
|
359
|
+
const state = api.getState();
|
|
360
|
+
expect(state.sort).toBeDefined();
|
|
361
|
+
expect(state.columnOrder).toBeDefined();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Cell Editing Tests
|
|
365
|
+
it('should update cell value on edit', () => {
|
|
366
|
+
const editApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
367
|
+
const firstNode = editApi.getDisplayedRowAtIndex(0);
|
|
368
|
+
if (!firstNode) return;
|
|
369
|
+
|
|
370
|
+
const originalName = firstNode.data.name;
|
|
371
|
+
|
|
372
|
+
// Simulate cell edit via transaction
|
|
373
|
+
const newValue = 'Updated Name';
|
|
374
|
+
editApi.applyTransaction({
|
|
375
|
+
update: [{ ...firstNode.data, name: newValue }]
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const updatedNode = editApi.getDisplayedRowAtIndex(0);
|
|
379
|
+
expect(updatedNode?.data.name).toBe(newValue);
|
|
380
|
+
expect(updatedNode?.data.name).not.toBe(originalName);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should support read-only cells', () => {
|
|
384
|
+
const readOnlyColumnDefs: ColDef[] = [
|
|
385
|
+
{ colId: 'id', field: 'id', headerName: 'ID', editable: false },
|
|
386
|
+
{ colId: 'name', field: 'name', headerName: 'Name', editable: true }
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
expect(readOnlyColumnDefs[0].editable).toBe(false);
|
|
390
|
+
expect(readOnlyColumnDefs[1].editable).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should support valueParser on column', () => {
|
|
394
|
+
const parserColumnDefs: ColDef[] = [
|
|
395
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
396
|
+
{ colId: 'value', field: 'value', headerName: 'Value',
|
|
397
|
+
valueParser: (params: any) => Number(params.newValue) }
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
expect(parserColumnDefs[1].valueParser).toBeDefined();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should support valueSetter on column', () => {
|
|
404
|
+
const setterColumnDefs: ColDef[] = [
|
|
405
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
406
|
+
{ colId: 'name', field: 'name', headerName: 'Name',
|
|
407
|
+
valueSetter: (params: any) => {
|
|
408
|
+
params.data.name = params.newValue.toUpperCase();
|
|
409
|
+
return true;
|
|
410
|
+
}}
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
expect(setterColumnDefs[1].valueSetter).toBeDefined();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Column Pinning Tests
|
|
417
|
+
it('should pin column to left', () => {
|
|
418
|
+
const pinColumnDefs: ColDef[] = [
|
|
419
|
+
{ colId: 'id', field: 'id', headerName: 'ID', pinned: 'left' },
|
|
420
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
421
|
+
{ colId: 'value', field: 'value', headerName: 'Value' }
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
|
|
425
|
+
const columns = pinApi.getAllColumns();
|
|
426
|
+
|
|
427
|
+
const pinnedCol = columns.find(c => c.pinned === 'left');
|
|
428
|
+
expect(pinnedCol).toBeDefined();
|
|
429
|
+
expect(pinnedCol?.colId).toBe('id');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should pin column to right', () => {
|
|
433
|
+
const pinColumnDefs: ColDef[] = [
|
|
434
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
435
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
436
|
+
{ colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' }
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
|
|
440
|
+
const columns = pinApi.getAllColumns();
|
|
441
|
+
|
|
442
|
+
const pinnedCol = columns.find(c => c.pinned === 'right');
|
|
443
|
+
expect(pinnedCol).toBeDefined();
|
|
444
|
+
expect(pinnedCol?.colId).toBe('value');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should return pinned columns in getColumnPinningState', () => {
|
|
448
|
+
const pinColumnDefs: ColDef[] = [
|
|
449
|
+
{ colId: 'id', field: 'id', headerName: 'ID', pinned: 'left' },
|
|
450
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
451
|
+
{ colId: 'value', field: 'value', headerName: 'Value', pinned: 'right' }
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const pinApi = service.createApi(pinColumnDefs, [...testRowData]);
|
|
455
|
+
const state = pinApi.getState();
|
|
456
|
+
|
|
457
|
+
expect(state.columnPinning).toBeDefined();
|
|
458
|
+
expect(state.columnPinning?.left).toContain('id');
|
|
459
|
+
expect(state.columnPinning?.right).toContain('value');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Row Pinning Tests
|
|
463
|
+
it('should pin row to top', () => {
|
|
464
|
+
const rowData: any[] = [
|
|
465
|
+
{ id: 1, name: 'Row 1' },
|
|
466
|
+
{ id: 2, name: 'Row 2', pinned: 'top' },
|
|
467
|
+
{ id: 3, name: 'Row 3' }
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
|
|
471
|
+
const displayedCount = pinApi.getDisplayedRowCount();
|
|
472
|
+
|
|
473
|
+
// Should have 3 rows total
|
|
474
|
+
expect(displayedCount).toBe(3);
|
|
475
|
+
|
|
476
|
+
// First row should be the pinned top one
|
|
477
|
+
const firstRow = pinApi.getDisplayedRowAtIndex(0);
|
|
478
|
+
expect(firstRow?.rowPinned).toBe('top');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should pin row to bottom', () => {
|
|
482
|
+
const rowData: any[] = [
|
|
483
|
+
{ id: 1, name: 'Row 1' },
|
|
484
|
+
{ id: 2, name: 'Row 2', pinned: 'bottom' },
|
|
485
|
+
{ id: 3, name: 'Row 3' }
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
|
|
489
|
+
|
|
490
|
+
// Last row should be the pinned bottom one
|
|
491
|
+
const lastRowIndex = pinApi.getDisplayedRowCount() - 1;
|
|
492
|
+
const lastRow = pinApi.getDisplayedRowAtIndex(lastRowIndex);
|
|
493
|
+
expect(lastRow?.rowPinned).toBe('bottom');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should order pinned rows correctly', () => {
|
|
497
|
+
const rowData: any[] = [
|
|
498
|
+
{ id: 1, name: 'Normal 1' },
|
|
499
|
+
{ id: 2, name: 'Top 1', pinned: 'top' },
|
|
500
|
+
{ id: 3, name: 'Normal 2' },
|
|
501
|
+
{ id: 4, name: 'Bottom 1', pinned: 'bottom' },
|
|
502
|
+
{ id: 5, name: 'Top 2', pinned: 'top' }
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
const pinApi = service.createApi(testColumnDefs.slice(0, 2), rowData);
|
|
506
|
+
|
|
507
|
+
// Pinned top rows should come first
|
|
508
|
+
expect(pinApi.getDisplayedRowAtIndex(0)?.rowPinned).toBe('top');
|
|
509
|
+
expect(pinApi.getDisplayedRowAtIndex(1)?.rowPinned).toBe('top');
|
|
510
|
+
|
|
511
|
+
// Normal rows in middle
|
|
512
|
+
expect(pinApi.getDisplayedRowAtIndex(2)?.rowPinned).toBe(false);
|
|
513
|
+
expect(pinApi.getDisplayedRowAtIndex(3)?.rowPinned).toBe(false);
|
|
514
|
+
|
|
515
|
+
// Pinned bottom rows at end
|
|
516
|
+
expect(pinApi.getDisplayedRowAtIndex(4)?.rowPinned).toBe('bottom');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should get displayed row count', () => {
|
|
520
|
+
const freshApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
521
|
+
expect(freshApi.getDisplayedRowCount()).toBe(3);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Selection API Tests
|
|
525
|
+
it('should select single row', () => {
|
|
526
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
527
|
+
const firstRow = selectApi.getDisplayedRowAtIndex(0);
|
|
528
|
+
if (!firstRow) return;
|
|
529
|
+
|
|
530
|
+
firstRow.selected = true;
|
|
531
|
+
const selected = selectApi.getSelectedRows();
|
|
532
|
+
|
|
533
|
+
expect(selected.length).toBe(1);
|
|
534
|
+
expect(selected[0].data.id).toBe(1);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should select multiple rows with Ctrl key', () => {
|
|
538
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
539
|
+
|
|
540
|
+
// Select first row
|
|
541
|
+
const firstRow = selectApi.getDisplayedRowAtIndex(0);
|
|
542
|
+
if (!firstRow) return;
|
|
543
|
+
firstRow.selected = true;
|
|
544
|
+
|
|
545
|
+
// Ctrl+click to select third row (multi-select)
|
|
546
|
+
const thirdRow = selectApi.getDisplayedRowAtIndex(2);
|
|
547
|
+
if (!thirdRow) return;
|
|
548
|
+
thirdRow.selected = true;
|
|
549
|
+
|
|
550
|
+
const selected = selectApi.getSelectedRows();
|
|
551
|
+
expect(selected.length).toBe(2);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should select all rows', () => {
|
|
555
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
556
|
+
selectApi.selectAll();
|
|
557
|
+
|
|
558
|
+
const selected = selectApi.getSelectedRows();
|
|
559
|
+
expect(selected.length).toBe(3);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should deselect all rows', () => {
|
|
563
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
564
|
+
selectApi.selectAll();
|
|
565
|
+
expect(selectApi.getSelectedRows().length).toBe(3);
|
|
566
|
+
|
|
567
|
+
selectApi.deselectAll();
|
|
568
|
+
expect(selectApi.getSelectedRows().length).toBe(0);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should toggle row selection', () => {
|
|
572
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
573
|
+
const row = selectApi.getDisplayedRowAtIndex(0);
|
|
574
|
+
if (!row) return;
|
|
575
|
+
|
|
576
|
+
row.selected = true;
|
|
577
|
+
expect(selectApi.getSelectedRows().length).toBe(1);
|
|
578
|
+
|
|
579
|
+
row.selected = false;
|
|
580
|
+
expect(selectApi.getSelectedRows().length).toBe(0);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should get selected row count', () => {
|
|
584
|
+
const selectApi = service.createApi(testColumnDefs, [...testRowData]);
|
|
585
|
+
selectApi.selectAll();
|
|
586
|
+
|
|
587
|
+
const selectedCount = selectApi.getSelectedRows().length;
|
|
588
|
+
expect(selectedCount).toBe(3);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should support row selection with checkbox', () => {
|
|
592
|
+
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' }
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
expect(selectionColumnDefs[0].checkboxSelection).toBe(true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Aggregation Tests
|
|
602
|
+
it('should calculate sum aggregation', () => {
|
|
603
|
+
const aggData: any[] = [
|
|
604
|
+
{ id: 1, name: 'Item 1', value: 100 },
|
|
605
|
+
{ id: 2, name: 'Item 2', value: 200 },
|
|
606
|
+
{ id: 3, name: 'Item 3', value: 300 }
|
|
607
|
+
];
|
|
608
|
+
const aggColumnDefs: ColDef[] = [
|
|
609
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
610
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
611
|
+
{ colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'sum' }
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
service.createApi(aggColumnDefs, aggData);
|
|
615
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
616
|
+
expect(agg['value']).toBe(600);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should calculate average aggregation', () => {
|
|
620
|
+
const aggData: any[] = [
|
|
621
|
+
{ id: 1, name: 'Item 1', value: 100 },
|
|
622
|
+
{ id: 2, name: 'Item 2', value: 200 },
|
|
623
|
+
{ id: 3, name: 'Item 3', value: 300 }
|
|
624
|
+
];
|
|
625
|
+
const aggColumnDefs: ColDef[] = [
|
|
626
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
627
|
+
{ colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'avg' }
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
service.createApi(aggColumnDefs, aggData);
|
|
631
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
632
|
+
expect(agg['value']).toBe(200);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should calculate min/max aggregation', () => {
|
|
636
|
+
const aggData: any[] = [
|
|
637
|
+
{ id: 1, name: 'Item 1', value: 100 },
|
|
638
|
+
{ id: 2, name: 'Item 2', value: 50 },
|
|
639
|
+
{ id: 3, name: 'Item 3', value: 300 }
|
|
640
|
+
];
|
|
641
|
+
const aggColumnDefs: ColDef[] = [
|
|
642
|
+
{ colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'min' }
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
service.createApi(aggColumnDefs, aggData);
|
|
646
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
647
|
+
expect(agg['value']).toBe(50);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should calculate max aggregation', () => {
|
|
651
|
+
const aggData: any[] = [
|
|
652
|
+
{ id: 1, name: 'Item 1', value: 100 },
|
|
653
|
+
{ id: 2, name: 'Item 2', value: 50 },
|
|
654
|
+
{ id: 3, name: 'Item 3', value: 300 }
|
|
655
|
+
];
|
|
656
|
+
const aggColumnDefs: ColDef[] = [
|
|
657
|
+
{ colId: 'value', field: 'value', headerName: 'Value', aggFunc: 'max' }
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
service.createApi(aggColumnDefs, aggData);
|
|
661
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
662
|
+
expect(agg['value']).toBe(300);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should calculate count aggregation', () => {
|
|
666
|
+
const aggData: any[] = [
|
|
667
|
+
{ id: 1, name: 'Item 1' },
|
|
668
|
+
{ id: 2, name: 'Item 2' },
|
|
669
|
+
{ id: 3, name: 'Item 3' }
|
|
670
|
+
];
|
|
671
|
+
const aggColumnDefs: ColDef[] = [
|
|
672
|
+
{ colId: 'id', field: 'id', headerName: 'ID', aggFunc: 'count' }
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
service.createApi(aggColumnDefs, aggData);
|
|
676
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
677
|
+
expect(agg['id']).toBe(3);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should support custom aggregation function', () => {
|
|
681
|
+
const aggData: any[] = [
|
|
682
|
+
{ id: 1, value: 100 },
|
|
683
|
+
{ id: 2, value: 200 },
|
|
684
|
+
{ id: 3, value: 300 }
|
|
685
|
+
];
|
|
686
|
+
const customAggFunc = (params: any) => {
|
|
687
|
+
return params.values.reduce((sum: number, v: number) => sum + v, 0) * 2;
|
|
688
|
+
};
|
|
689
|
+
const aggColumnDefs: ColDef[] = [
|
|
690
|
+
{ colId: 'value', field: 'value', headerName: 'Value', aggFunc: customAggFunc }
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
service.createApi(aggColumnDefs, aggData);
|
|
694
|
+
const agg = service.calculateColumnAggregations(aggData);
|
|
695
|
+
expect(agg['value']).toBe(1200); // (100+200+300) * 2
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Excel Export Tests
|
|
699
|
+
it('should export data as CSV', () => {
|
|
700
|
+
const exportData: any[] = [
|
|
701
|
+
{ id: 1, name: 'John', email: 'john@example.com' },
|
|
702
|
+
{ id: 2, name: 'Jane', email: 'jane@example.com' }
|
|
703
|
+
];
|
|
704
|
+
const exportColumnDefs: ColDef[] = [
|
|
705
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
706
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
707
|
+
{ colId: 'email', field: 'email', headerName: 'Email' }
|
|
708
|
+
];
|
|
709
|
+
|
|
710
|
+
const exportApi = service.createApi(exportColumnDefs, exportData);
|
|
711
|
+
|
|
712
|
+
// Mock downloadFile to avoid browser API issues in tests
|
|
713
|
+
(service as any).downloadFile = vi.fn();
|
|
714
|
+
expect(() => exportApi.exportDataAsCsv()).not.toThrow();
|
|
715
|
+
expect((service as any).downloadFile).toHaveBeenCalled();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should export with custom filename', () => {
|
|
719
|
+
const exportData: any[] = [{ id: 1, name: 'Test' }];
|
|
720
|
+
const exportColumnDefs: ColDef[] = [
|
|
721
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
722
|
+
{ colId: 'name', field: 'name', headerName: 'Name' }
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const exportApi = service.createApi(exportColumnDefs, exportData);
|
|
726
|
+
(service as any).downloadFile = vi.fn();
|
|
727
|
+
|
|
728
|
+
exportApi.exportDataAsCsv({ fileName: 'custom-export.csv' });
|
|
729
|
+
expect((service as any).downloadFile).toHaveBeenCalledWith(
|
|
730
|
+
expect.any(String),
|
|
731
|
+
'custom-export.csv',
|
|
732
|
+
expect.any(String)
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('should export only selected columns', () => {
|
|
737
|
+
const exportData: any[] = [
|
|
738
|
+
{ id: 1, name: 'John', email: 'john@example.com' },
|
|
739
|
+
{ id: 2, name: 'Jane', email: 'jane@example.com' }
|
|
740
|
+
];
|
|
741
|
+
const exportColumnDefs: ColDef[] = [
|
|
742
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
743
|
+
{ colId: 'name', field: 'name', headerName: 'Name' },
|
|
744
|
+
{ colId: 'email', field: 'email', headerName: 'Email' }
|
|
745
|
+
];
|
|
746
|
+
|
|
747
|
+
const exportApi = service.createApi(exportColumnDefs, exportData);
|
|
748
|
+
(service as any).downloadFile = vi.fn();
|
|
749
|
+
|
|
750
|
+
exportApi.exportDataAsCsv({ columnKeys: ['id', 'name'] });
|
|
751
|
+
const csvContent = (service as any).downloadFile.mock.calls[0][0];
|
|
752
|
+
// Should not contain email column
|
|
753
|
+
expect(csvContent).not.toContain('email');
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('should skip headers when specified', () => {
|
|
757
|
+
const exportData: any[] = [{ id: 1, name: 'Test' }];
|
|
758
|
+
const exportColumnDefs: ColDef[] = [
|
|
759
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
760
|
+
{ colId: 'name', field: 'name', headerName: 'Name' }
|
|
761
|
+
];
|
|
762
|
+
|
|
763
|
+
const exportApi = service.createApi(exportColumnDefs, exportData);
|
|
764
|
+
(service as any).downloadFile = vi.fn();
|
|
765
|
+
|
|
766
|
+
exportApi.exportDataAsCsv({ skipHeader: true });
|
|
767
|
+
const csvContent = (service as any).downloadFile.mock.calls[0][0];
|
|
768
|
+
// First line should be data, not header
|
|
769
|
+
expect(csvContent).not.toMatch(/^ID,/);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('should export data as Excel', async () => {
|
|
773
|
+
const exportData: any[] = [{ id: 1, name: 'Test' }];
|
|
774
|
+
const exportColumnDefs: ColDef[] = [
|
|
775
|
+
{ colId: 'id', field: 'id', headerName: 'ID' },
|
|
776
|
+
{ colId: 'name', field: 'name', headerName: 'Name' }
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
const exportApi = service.createApi(exportColumnDefs, exportData);
|
|
780
|
+
|
|
781
|
+
// Mock URL methods
|
|
782
|
+
if (typeof URL.createObjectURL === 'undefined') {
|
|
783
|
+
URL.createObjectURL = vi.fn().mockReturnValue('blob:test');
|
|
784
|
+
}
|
|
785
|
+
if (typeof URL.revokeObjectURL === 'undefined') {
|
|
786
|
+
URL.revokeObjectURL = vi.fn();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// We expect it not to throw during the setup phase
|
|
790
|
+
expect(() => exportApi.exportDataAsExcel()).not.toThrow();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should get displayed row at index', () => {
|
|
794
|
+
const sortedApi = service.createApi(testColumnDefs, [
|
|
795
|
+
{ id: 10, name: 'First', age: 20, email: 'first@example.com' },
|
|
796
|
+
{ id: 20, name: 'Second', age: 25, email: 'second@example.com' }
|
|
797
|
+
]);
|
|
798
|
+
const row = sortedApi.getDisplayedRowAtIndex(1);
|
|
799
|
+
expect(row).toBeTruthy();
|
|
800
|
+
expect(row?.data.id).toBe(20);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should have unique grid id', () => {
|
|
804
|
+
const gridId = api.getGridId();
|
|
805
|
+
expect(gridId).toMatch(/argent-grid-[a-z0-9]{9}/);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
describe('Corner Cases', () => {
|
|
809
|
+
it('should handle empty row data', () => {
|
|
810
|
+
const emptyApi = service.createApi(testColumnDefs, []);
|
|
811
|
+
expect(emptyApi.getDisplayedRowCount()).toBe(0);
|
|
812
|
+
expect(emptyApi.getRowData()).toEqual([]);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('should handle null/undefined row data', () => {
|
|
816
|
+
const nullApi = service.createApi(testColumnDefs, null);
|
|
817
|
+
expect(nullApi.getDisplayedRowCount()).toBe(0);
|
|
818
|
+
expect(nullApi.getRowData()).toEqual([]);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should support custom getRowId in gridOptions', () => {
|
|
822
|
+
const data = [
|
|
823
|
+
{ customId: 'A', name: 'John' },
|
|
824
|
+
{ customId: 'B', name: 'Jane' }
|
|
825
|
+
];
|
|
826
|
+
const customApi = service.createApi(testColumnDefs, data, {
|
|
827
|
+
getRowId: (params) => params.data.customId
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
expect(customApi.getDisplayedRowCount()).toBe(2);
|
|
831
|
+
expect(customApi.getRowNode('A')).toBeTruthy();
|
|
832
|
+
expect(customApi.getRowNode('B')).toBeTruthy();
|
|
833
|
+
expect(customApi.getRowNode('A')?.data.name).toBe('John');
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('should handle rows with missing fields', () => {
|
|
837
|
+
const data = [
|
|
838
|
+
{ id: 1, name: 'John' },
|
|
839
|
+
{ id: 2, age: 30 }
|
|
840
|
+
];
|
|
841
|
+
const missingApi = service.createApi(testColumnDefs, data);
|
|
842
|
+
expect(missingApi.getDisplayedRowCount()).toBe(2);
|
|
843
|
+
|
|
844
|
+
// Test sorting on missing field
|
|
845
|
+
missingApi.setSortModel([{ colId: 'age', sort: 'asc' }]);
|
|
846
|
+
// John (undefined age) should be at the end according to compareValues
|
|
847
|
+
expect(missingApi.getDisplayedRowAtIndex(0)?.data.id).toBe(2);
|
|
848
|
+
expect(missingApi.getDisplayedRowAtIndex(1)?.data.id).toBe(1);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should handle duplicate IDs (last one wins in map)', () => {
|
|
852
|
+
const data = [
|
|
853
|
+
{ id: 'dup', name: 'First' },
|
|
854
|
+
{ id: 'dup', name: 'Second' }
|
|
855
|
+
];
|
|
856
|
+
const dupApi = service.createApi(testColumnDefs, data);
|
|
857
|
+
|
|
858
|
+
expect(dupApi.getDisplayedRowCount()).toBe(2);
|
|
859
|
+
|
|
860
|
+
const node = dupApi.getRowNode('dup');
|
|
861
|
+
expect(node?.data.name).toBe('Second');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('should handle update transaction for non-existent row', () => {
|
|
865
|
+
const api = service.createApi(testColumnDefs, [{ id: 1, name: 'John' }]);
|
|
866
|
+
const result = api.applyTransaction({
|
|
867
|
+
update: [{ id: 99, name: 'Missing' }]
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
expect(result?.update.length).toBe(0);
|
|
871
|
+
expect(api.getDisplayedRowCount()).toBe(1);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('should handle sorting on non-existent column', () => {
|
|
875
|
+
const api = service.createApi(testColumnDefs, [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]);
|
|
876
|
+
// Should not crash
|
|
877
|
+
api.setSortModel([{ colId: 'invalid', sort: 'asc' }]);
|
|
878
|
+
expect(api.getDisplayedRowCount()).toBe(2);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('should preserve selection across transactions', () => {
|
|
882
|
+
const api = service.createApi(testColumnDefs, [
|
|
883
|
+
{ id: 1, name: 'John' },
|
|
884
|
+
{ id: 2, name: 'Jane' }
|
|
885
|
+
]);
|
|
886
|
+
|
|
887
|
+
const node1 = api.getRowNode('1')!;
|
|
888
|
+
node1.selected = true;
|
|
889
|
+
|
|
890
|
+
api.applyTransaction({ add: [{ id: 3, name: 'Bob' }] });
|
|
891
|
+
|
|
892
|
+
const sameNode1 = api.getRowNode('1')!;
|
|
893
|
+
expect(sameNode1.selected).toBe(true);
|
|
894
|
+
expect(api.getSelectedNodes().length).toBe(1);
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
describe('Pivoting', () => {
|
|
899
|
+
const pivotColumnDefs: ColDef[] = [
|
|
900
|
+
{ field: 'id', headerName: 'ID' },
|
|
901
|
+
{ field: 'name', headerName: 'Name' },
|
|
902
|
+
{ field: 'dept', headerName: 'Dept', rowGroup: true },
|
|
903
|
+
{ field: 'location', headerName: 'Location', pivot: true },
|
|
904
|
+
{ field: 'salary', headerName: 'Salary', aggFunc: 'sum' }
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
const pivotData: any[] = [
|
|
908
|
+
{ id: 1, name: 'John', dept: 'Engineering', location: 'NY', salary: 1000 },
|
|
909
|
+
{ id: 2, name: 'Jane', dept: 'Engineering', location: 'SF', salary: 2000 },
|
|
910
|
+
{ id: 3, name: 'Bob', dept: 'Sales', location: 'NY', salary: 1500 },
|
|
911
|
+
{ id: 4, name: 'Alice', dept: 'Sales', location: 'SF', salary: 2500 },
|
|
912
|
+
{ id: 5, name: 'Charlie', dept: 'Engineering', location: 'NY', salary: 1200 }
|
|
913
|
+
];
|
|
914
|
+
|
|
915
|
+
it('should generate pivot columns correctly', () => {
|
|
916
|
+
const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: true });
|
|
917
|
+
const columns = api.getAllColumns();
|
|
918
|
+
const visibleColumns = columns.filter(c => c.visible);
|
|
919
|
+
|
|
920
|
+
// Auto Group + 2 pivot columns (NY, SF) = 3
|
|
921
|
+
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();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('should calculate pivoted values correctly', () => {
|
|
927
|
+
const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: true });
|
|
928
|
+
|
|
929
|
+
let engNode = null;
|
|
930
|
+
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
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
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);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should toggle pivot mode via API', () => {
|
|
944
|
+
const api = service.createApi(pivotColumnDefs, pivotData, { pivotMode: false });
|
|
945
|
+
expect(api.isPivotMode()).toBe(false);
|
|
946
|
+
|
|
947
|
+
api.setPivotMode(true);
|
|
948
|
+
expect(api.isPivotMode()).toBe(true);
|
|
949
|
+
expect(api.getAllColumns().filter(c => c.visible).some(c => c.colId.startsWith('pivot_'))).toBe(true);
|
|
950
|
+
|
|
951
|
+
api.setPivotMode(false);
|
|
952
|
+
expect(api.isPivotMode()).toBe(false);
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
describe('Master/Detail', () => {
|
|
957
|
+
const mdColumnDefs: ColDef[] = [
|
|
958
|
+
{ field: 'id', headerName: 'ID' },
|
|
959
|
+
{ field: 'name', headerName: 'Name' }
|
|
960
|
+
];
|
|
961
|
+
|
|
962
|
+
const mdData: any[] = [
|
|
963
|
+
{ id: 1, name: 'John' },
|
|
964
|
+
{ id: 2, name: 'Jane' }
|
|
965
|
+
];
|
|
966
|
+
|
|
967
|
+
it('should identify master rows correctly', () => {
|
|
968
|
+
const api = service.createApi(mdColumnDefs, mdData, {
|
|
969
|
+
masterDetail: true,
|
|
970
|
+
isRowMaster: (data) => data.id === 1
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
const node1 = api.getRowNode('1');
|
|
974
|
+
const node2 = api.getRowNode('2');
|
|
975
|
+
|
|
976
|
+
expect(node1?.master).toBe(true);
|
|
977
|
+
expect(node2?.master).toBe(false);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('should insert detail row when master is expanded', () => {
|
|
981
|
+
const api = service.createApi(mdColumnDefs, mdData, {
|
|
982
|
+
masterDetail: true,
|
|
983
|
+
isRowMaster: (data) => data.id === 1
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
expect(api.getDisplayedRowCount()).toBe(2);
|
|
987
|
+
|
|
988
|
+
const node1 = api.getRowNode('1')!;
|
|
989
|
+
api.setRowNodeExpanded(node1, true);
|
|
990
|
+
|
|
991
|
+
// Should now have 3 rows: Master 1, Detail 1, Master 2
|
|
992
|
+
expect(api.getDisplayedRowCount()).toBe(3);
|
|
993
|
+
|
|
994
|
+
const detailNode = api.getDisplayedRowAtIndex(1);
|
|
995
|
+
expect(detailNode?.detail).toBe(true);
|
|
996
|
+
expect(detailNode?.id).toBe('1-detail');
|
|
997
|
+
expect(detailNode?.masterRowNode).toBe(node1);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should remove detail row when master is collapsed', () => {
|
|
1001
|
+
const api = service.createApi(mdColumnDefs, mdData, {
|
|
1002
|
+
masterDetail: true,
|
|
1003
|
+
isRowMaster: (data) => data.id === 1
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const node1 = api.getRowNode('1')!;
|
|
1007
|
+
api.setRowNodeExpanded(node1, true);
|
|
1008
|
+
expect(api.getDisplayedRowCount()).toBe(3);
|
|
1009
|
+
|
|
1010
|
+
api.setRowNodeExpanded(node1, false);
|
|
1011
|
+
expect(api.getDisplayedRowCount()).toBe(2);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should calculate correct Y positions for variable heights', () => {
|
|
1015
|
+
const api = service.createApi(mdColumnDefs, mdData, {
|
|
1016
|
+
masterDetail: true,
|
|
1017
|
+
isRowMaster: (data) => data.id === 1,
|
|
1018
|
+
rowHeight: 30,
|
|
1019
|
+
detailRowHeight: 100
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const node1 = api.getRowNode('1')!;
|
|
1023
|
+
api.setRowNodeExpanded(node1, true);
|
|
1024
|
+
|
|
1025
|
+
// Row 0: Master (Y=0, H=30)
|
|
1026
|
+
// Row 1: Detail (Y=30, H=100)
|
|
1027
|
+
// Row 2: Master (Y=130, H=30)
|
|
1028
|
+
|
|
1029
|
+
expect(api.getRowY(0)).toBe(0);
|
|
1030
|
+
expect(api.getRowY(1)).toBe(30);
|
|
1031
|
+
expect(api.getRowY(2)).toBe(130);
|
|
1032
|
+
expect(api.getTotalHeight()).toBe(160);
|
|
1033
|
+
|
|
1034
|
+
expect(api.getRowAtY(15)).toBe(0);
|
|
1035
|
+
expect(api.getRowAtY(50)).toBe(1);
|
|
1036
|
+
expect(api.getRowAtY(140)).toBe(2);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
});
|