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.
Files changed (53) hide show
  1. package/.github/workflows/pages.yml +68 -0
  2. package/AGENTS.md +179 -0
  3. package/README.md +222 -0
  4. package/demo-app/README.md +70 -0
  5. package/demo-app/angular.json +78 -0
  6. package/demo-app/e2e/benchmark.spec.ts +53 -0
  7. package/demo-app/e2e/demo-page.spec.ts +77 -0
  8. package/demo-app/e2e/grid-features.spec.ts +269 -0
  9. package/demo-app/package-lock.json +14023 -0
  10. package/demo-app/package.json +36 -0
  11. package/demo-app/playwright-test-menu.js +19 -0
  12. package/demo-app/playwright.config.ts +23 -0
  13. package/demo-app/src/app/app.component.ts +10 -0
  14. package/demo-app/src/app/app.config.ts +13 -0
  15. package/demo-app/src/app/app.routes.ts +7 -0
  16. package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
  17. package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
  18. package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
  19. package/demo-app/src/index.html +19 -0
  20. package/demo-app/src/main.ts +6 -0
  21. package/demo-app/tsconfig.json +31 -0
  22. package/ng-package.json +8 -0
  23. package/package.json +60 -0
  24. package/plan.md +131 -0
  25. package/setup-vitest.ts +18 -0
  26. package/src/lib/argent-grid.module.ts +21 -0
  27. package/src/lib/components/argent-grid.component.css +483 -0
  28. package/src/lib/components/argent-grid.component.html +320 -0
  29. package/src/lib/components/argent-grid.component.spec.ts +189 -0
  30. package/src/lib/components/argent-grid.component.ts +1188 -0
  31. package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
  32. package/src/lib/rendering/canvas-renderer.ts +962 -0
  33. package/src/lib/rendering/render/blit.spec.ts +453 -0
  34. package/src/lib/rendering/render/blit.ts +393 -0
  35. package/src/lib/rendering/render/cells.ts +369 -0
  36. package/src/lib/rendering/render/index.ts +105 -0
  37. package/src/lib/rendering/render/lines.ts +363 -0
  38. package/src/lib/rendering/render/theme.spec.ts +282 -0
  39. package/src/lib/rendering/render/theme.ts +201 -0
  40. package/src/lib/rendering/render/types.ts +279 -0
  41. package/src/lib/rendering/render/walk.spec.ts +360 -0
  42. package/src/lib/rendering/render/walk.ts +360 -0
  43. package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
  44. package/src/lib/rendering/utils/damage-tracker.ts +423 -0
  45. package/src/lib/rendering/utils/index.ts +7 -0
  46. package/src/lib/services/grid.service.spec.ts +1039 -0
  47. package/src/lib/services/grid.service.ts +1284 -0
  48. package/src/lib/types/ag-grid-types.ts +970 -0
  49. package/src/public-api.ts +22 -0
  50. package/tsconfig.json +32 -0
  51. package/tsconfig.lib.json +11 -0
  52. package/tsconfig.spec.json +8 -0
  53. package/vitest.config.ts +55 -0
@@ -0,0 +1,320 @@
1
+ <div class="argent-grid-container" [style.height]="height" [style.width]="width" (click)="onContainerClick($event)">
2
+ <div class="argent-grid-main-layout">
3
+ <div class="argent-grid-content-area">
4
+ <!-- Header Layer (DOM-based for accessibility) -->
5
+ <div class="argent-grid-header">
6
+ <!-- Main Header Row -->
7
+ <div class="argent-grid-header-row">
8
+ <!-- Selection Column Header -->
9
+ <div
10
+ *ngIf="showSelectionColumn"
11
+ class="argent-grid-header-cell argent-grid-selection-header"
12
+ [style.width.px]="selectionColumnWidth"
13
+ (click)="onSelectionHeaderClick()">
14
+ <input type="checkbox"
15
+ [checked]="isAllSelected"
16
+ [indeterminate]="isIndeterminateSelection"
17
+ (change)="onSelectionHeaderChange($event)" />
18
+ </div>
19
+
20
+ <!-- Left Pinned Columns -->
21
+ <div class="argent-grid-header-pinned-left-container"
22
+ cdkDropList
23
+ id="left-pinned"
24
+ [cdkDropListConnectedTo]="['scrollable', 'right-pinned']"
25
+ cdkDropListOrientation="horizontal"
26
+ (cdkDropListDropped)="onColumnDropped($event, 'left')">
27
+ <div
28
+ *ngFor="let col of getLeftPinnedColumns(); trackBy: trackByColumn"
29
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-left"
30
+ [style.width.px]="getColumnWidth(col)"
31
+ [class.sortable]="isSortable(col)"
32
+ (click)="onHeaderClick(col)"
33
+ cdkDrag
34
+ [cdkDragData]="col">
35
+ <div class="argent-grid-header-content" cdkDragHandle>
36
+ <span class="header-text">{{ getHeaderName(col) }}</span>
37
+ <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
38
+ </div>
39
+ <div class="argent-grid-header-menu-icon" (click)="onHeaderMenuClick($event, col)" *ngIf="hasHeaderMenu(col)">
40
+ &#8942;
41
+ </div>
42
+ <div class="argent-grid-header-resize-handle"
43
+ *ngIf="isResizable(col)"
44
+ [class.resizing]="isResizing && resizeColumn === col"
45
+ (mousedown)="onResizeMouseDown($event, col)">
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- Scrollable Columns -->
51
+ <div class="argent-grid-header-scrollable"
52
+ #headerScrollable
53
+ cdkDropList
54
+ id="scrollable"
55
+ [cdkDropListConnectedTo]="['left-pinned', 'right-pinned']"
56
+ cdkDropListOrientation="horizontal"
57
+ (cdkDropListDropped)="onColumnDropped($event, 'none')">
58
+ <div class="argent-grid-header-row">
59
+ <div
60
+ *ngFor="let col of getNonPinnedColumns(); trackBy: trackByColumn"
61
+ class="argent-grid-header-cell"
62
+ [style.width.px]="getColumnWidth(col)"
63
+ [class.sortable]="isSortable(col)"
64
+ (click)="onHeaderClick(col)"
65
+ cdkDrag
66
+ [cdkDragData]="col">
67
+ <div class="argent-grid-header-content" cdkDragHandle>
68
+ <span class="header-text">{{ getHeaderName(col) }}</span>
69
+ <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
70
+ </div>
71
+ <div class="argent-grid-header-menu-icon" (click)="onHeaderMenuClick($event, col)" *ngIf="hasHeaderMenu(col)">
72
+ &#8942;
73
+ </div>
74
+ <div class="argent-grid-header-resize-handle"
75
+ *ngIf="isResizable(col)"
76
+ [class.resizing]="isResizing && resizeColumn === col"
77
+ (mousedown)="onResizeMouseDown($event, col)">
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Right Pinned Columns -->
84
+ <div class="argent-grid-header-pinned-right-container"
85
+ cdkDropList
86
+ id="right-pinned"
87
+ [cdkDropListConnectedTo]="['left-pinned', 'scrollable']"
88
+ cdkDropListOrientation="horizontal"
89
+ (cdkDropListDropped)="onColumnDropped($event, 'right')">
90
+ <div
91
+ *ngFor="let col of getRightPinnedColumns(); trackBy: trackByColumn"
92
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-right"
93
+ [style.width.px]="getColumnWidth(col)"
94
+ [class.sortable]="isSortable(col)"
95
+ (click)="onHeaderClick(col)"
96
+ cdkDrag
97
+ [cdkDragData]="col">
98
+ <div class="argent-grid-header-content" cdkDragHandle>
99
+ <span class="header-text">{{ getHeaderName(col) }}</span>
100
+ <span class="sort-indicator" *ngIf="getSortIndicator(col)">{{ getSortIndicator(col) }}</span>
101
+ </div>
102
+ <div class="argent-grid-header-menu-icon" (click)="onHeaderMenuClick($event, col)" *ngIf="hasHeaderMenu(col)">
103
+ &#8942;
104
+ </div>
105
+ <div class="argent-grid-header-resize-handle"
106
+ *ngIf="isResizable(col)"
107
+ [class.resizing]="isResizing && resizeColumn === col"
108
+ (mousedown)="onResizeMouseDown($event, col)">
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Floating Filter Row -->
115
+ <div class="argent-grid-header-row floating-filter-row" *ngIf="hasFloatingFilters()">
116
+ <!-- Selection Column Padding -->
117
+ <div *ngIf="showSelectionColumn" class="argent-grid-header-cell" [style.width.px]="selectionColumnWidth"></div>
118
+
119
+ <!-- Left Pinned Filters -->
120
+ <div class="argent-grid-header-pinned-left-container">
121
+ <div
122
+ *ngFor="let col of getLeftPinnedColumns(); trackBy: trackByColumn"
123
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-left"
124
+ [style.width.px]="getColumnWidth(col)">
125
+ <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
126
+ <input #filterInput
127
+ class="floating-filter-input"
128
+ [type]="getFilterInputType(col)"
129
+ [value]="getFloatingFilterValue(col)"
130
+ (input)="onFloatingFilterInput($event, col)"
131
+ [placeholder]="'Filter...'" />
132
+ <span class="floating-filter-clear"
133
+ *ngIf="hasFilterValue(col, filterInput)"
134
+ (click)="clearFloatingFilter(col, filterInput)">✕</span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Scrollable Filters -->
140
+ <div class="argent-grid-header-scrollable" #headerScrollableFilter>
141
+ <div class="argent-grid-header-row">
142
+ <div
143
+ *ngFor="let col of getNonPinnedColumns(); trackBy: trackByColumn"
144
+ class="argent-grid-header-cell"
145
+ [style.width.px]="getColumnWidth(col)">
146
+ <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
147
+ <input #filterInput
148
+ class="floating-filter-input"
149
+ [type]="getFilterInputType(col)"
150
+ [value]="getFloatingFilterValue(col)"
151
+ (input)="onFloatingFilterInput($event, col)"
152
+ [placeholder]="'Filter...'" />
153
+ <span class="floating-filter-clear"
154
+ *ngIf="hasFilterValue(col, filterInput)"
155
+ (click)="clearFloatingFilter(col, filterInput)">✕</span>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Right Pinned Filters -->
162
+ <div class="argent-grid-header-pinned-right-container">
163
+ <div
164
+ *ngFor="let col of getRightPinnedColumns(); trackBy: trackByColumn"
165
+ class="argent-grid-header-cell argent-grid-header-cell-pinned-right"
166
+ [style.width.px]="getColumnWidth(col)">
167
+ <div class="floating-filter-container" *ngIf="isFloatingFilterEnabled(col)">
168
+ <input #filterInput
169
+ class="floating-filter-input"
170
+ [type]="getFilterInputType(col)"
171
+ [value]="getFloatingFilterValue(col)"
172
+ (input)="onFloatingFilterInput($event, col)"
173
+ [placeholder]="'Filter...'" />
174
+ <span class="floating-filter-clear"
175
+ *ngIf="hasFilterValue(col, filterInput)"
176
+ (click)="clearFloatingFilter(col, filterInput)">✕</span>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <!-- Canvas Layer for Data Viewport with virtual scrolling -->
184
+ <div class="argent-grid-viewport" #viewport>
185
+ <!-- Spacer to create scrollbars for virtual scrolling -->
186
+ <div class="argent-grid-scroll-spacer" [style.height.px]="totalHeight" [style.width.px]="totalWidth"></div>
187
+
188
+ <canvas #gridCanvas class="argent-grid-canvas" (contextmenu)="onCanvasContextMenu($event)"></canvas>
189
+
190
+ <!-- Cell Editor Overlay -->
191
+ <div class="argent-grid-cell-editor"
192
+ *ngIf="isEditing"
193
+ [style.top.px]="editorPosition.y"
194
+ [style.left.px]="editorPosition.x"
195
+ [style.width.px]="editorPosition.width"
196
+ [style.height.px]="editorPosition.height"
197
+ (click)="$event.stopPropagation()">
198
+ <input #editorInput
199
+ type="text"
200
+ class="argent-grid-editor-input"
201
+ [value]="editingValue"
202
+ (input)="onEditorInput($event)"
203
+ (keydown)="onEditorKeydown($event)"
204
+ (blur)="onEditorBlur()"
205
+ autofocus />
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Side Bar / Tool Panels -->
211
+ <div class="argent-grid-side-bar" *ngIf="sideBarVisible" [class.has-active-panel]="!!activeToolPanel">
212
+ <div class="side-bar-buttons">
213
+ <div class="side-bar-button" [class.active]="activeToolPanel === 'columns'" (click)="toggleToolPanel('columns')">
214
+ Columns
215
+ </div>
216
+ <div class="side-bar-button" [class.active]="activeToolPanel === 'filters'" (click)="toggleToolPanel('filters')">
217
+ Filters
218
+ </div>
219
+ </div>
220
+
221
+ <div class="tool-panel-content" *ngIf="activeToolPanel">
222
+ <!-- Columns Tool Panel -->
223
+ <div class="columns-tool-panel" *ngIf="activeToolPanel === 'columns'">
224
+ <h3>Columns</h3>
225
+ <div class="column-list"
226
+ cdkDropList
227
+ (cdkDropListDropped)="onSidebarColumnDropped($event)">
228
+ <div *ngFor="let col of getAllColumns()"
229
+ class="column-item"
230
+ cdkDrag
231
+ [cdkDragData]="col">
232
+ <div *cdkDragPreview class="sidebar-drag-preview">
233
+ <span class="column-drag-handle">⠿</span>
234
+ <span>{{ getHeaderName(col) }}</span>
235
+ </div>
236
+ <div *cdkDragPlaceholder class="sidebar-drag-placeholder"></div>
237
+
238
+ <span class="column-drag-handle" cdkDragHandle>⠿</span>
239
+ <input type="checkbox" [checked]="col.visible" (change)="toggleColumnVisibility(col)" />
240
+ <span class="column-label">{{ getHeaderName(col) }}</span>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Filters Tool Panel -->
246
+ <div class="filters-tool-panel" *ngIf="activeToolPanel === 'filters'">
247
+ <h3>Filters</h3>
248
+ <div class="filter-placeholder">
249
+ Filters coming soon...
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ <!-- Overlay for loading/no rows -->
257
+ <div class="argent-grid-overlay" *ngIf="showOverlay">
258
+ <ng-content select="[overlay]"></ng-content>
259
+ </div>
260
+
261
+ <!-- Header Menu Overlay -->
262
+ <div class="argent-grid-header-menu"
263
+ *ngIf="activeHeaderMenu"
264
+ [style.top.px]="headerMenuPosition.y"
265
+ [style.left.px]="headerMenuPosition.x"
266
+ (click)="$event.stopPropagation()">
267
+ <div class="menu-item" (click)="sortColumnMenu('asc')">
268
+ <span class="menu-icon">↑</span> Sort Ascending
269
+ </div>
270
+ <div class="menu-item" (click)="sortColumnMenu('desc')">
271
+ <span class="menu-icon">↓</span> Sort Descending
272
+ </div>
273
+ <div class="menu-item" (click)="sortColumnMenu(null)">
274
+ <span class="menu-icon">✕</span> Clear Sort
275
+ </div>
276
+ <div class="menu-divider"></div>
277
+ <div class="menu-item" (click)="hideColumnMenu()">
278
+ <span class="menu-icon">ø</span> Hide Column
279
+ </div>
280
+ <div class="menu-item" (click)="pinColumnMenu('left')">
281
+ <span class="menu-icon">«</span> Pin Left
282
+ </div>
283
+ <div class="menu-item" (click)="pinColumnMenu('right')">
284
+ <span class="menu-icon">»</span> Pin Right
285
+ </div>
286
+ <div class="menu-item" (click)="pinColumnMenu(null)">
287
+ <span class="menu-icon">↺</span> Unpin
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Context Menu Overlay -->
292
+ <div class="argent-grid-context-menu"
293
+ *ngIf="activeContextMenu"
294
+ [style.top.px]="contextMenuPosition.y"
295
+ [style.left.px]="contextMenuPosition.x"
296
+ (click)="$event.stopPropagation()">
297
+ <ng-container *ngFor="let item of contextMenuItems">
298
+ <div *ngIf="item.separator" class="menu-divider"></div>
299
+ <div *ngIf="!item.separator"
300
+ class="menu-item"
301
+ [class.disabled]="item.disabled"
302
+ [class.has-submenu]="item.subMenu && item.subMenu.length > 0"
303
+ (click)="!item.disabled && item.action(); !item.subMenu && closeContextMenu()">
304
+ <span class="menu-icon" *ngIf="item.icon">{{ item.icon }}</span>
305
+ <span class="menu-text">{{ item.name }}</span>
306
+ <span class="menu-arrow" *ngIf="item.subMenu && item.subMenu.length > 0">▶</span>
307
+
308
+ <!-- Sub-menu -->
309
+ <div class="argent-grid-context-menu sub-menu" *ngIf="item.subMenu && item.subMenu.length > 0">
310
+ <div *ngFor="let subItem of item.subMenu"
311
+ class="menu-item"
312
+ (click)="subItem.action(); closeContextMenu(); $event.stopPropagation()">
313
+ <span class="menu-icon" *ngIf="subItem.icon">{{ subItem.icon }}</span>
314
+ <span class="menu-text">{{ subItem.name }}</span>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ </ng-container>
319
+ </div>
320
+ </div>
@@ -0,0 +1,189 @@
1
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
2
+ import { ChangeDetectorRef, provideExperimentalZonelessChangeDetection } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import { DragDropModule } from '@angular/cdk/drag-drop';
5
+ import { ArgentGridComponent } from './argent-grid.component';
6
+ import { GridService } from '../services/grid.service';
7
+ import { ColDef } from '../types/ag-grid-types';
8
+
9
+ // Mock canvas context
10
+ const mockCanvasContext = {
11
+ clearRect: vi.fn(),
12
+ fillRect: vi.fn(),
13
+ beginPath: vi.fn(),
14
+ moveTo: vi.fn(),
15
+ lineTo: vi.fn(),
16
+ stroke: vi.fn(),
17
+ fillText: vi.fn(),
18
+ measureText: vi.fn(() => ({ width: 100 })),
19
+ scale: vi.fn(),
20
+ setTransform: vi.fn(),
21
+ font: '13px sans-serif',
22
+ textBaseline: 'middle',
23
+ fillStyle: '#000',
24
+ strokeStyle: '#000'
25
+ };
26
+
27
+ const mockCanvas = {
28
+ getContext: vi.fn(() => mockCanvasContext as any),
29
+ width: 800,
30
+ height: 600,
31
+ style: {},
32
+ addEventListener: vi.fn(),
33
+ getBoundingClientRect: vi.fn(() => ({ width: 800, height: 600 }))
34
+ } as unknown as HTMLCanvasElement;
35
+
36
+ interface TestData {
37
+ id: number;
38
+ name: string;
39
+ value: number;
40
+ }
41
+
42
+ describe('ArgentGridComponent', () => {
43
+ let component: ArgentGridComponent<TestData>;
44
+ let fixture: ComponentFixture<ArgentGridComponent<TestData>>;
45
+
46
+ // Mock getContext globally for this test suite
47
+ beforeAll(() => {
48
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation((contextId) => {
49
+ if (contextId === '2d') {
50
+ return mockCanvasContext as any;
51
+ }
52
+ return null;
53
+ });
54
+
55
+ vi.spyOn(HTMLCanvasElement.prototype, 'getBoundingClientRect').mockReturnValue({
56
+ width: 800,
57
+ height: 600,
58
+ top: 0,
59
+ left: 0,
60
+ bottom: 600,
61
+ right: 800,
62
+ x: 0,
63
+ y: 0,
64
+ toJSON: () => {}
65
+ } as DOMRect);
66
+ });
67
+
68
+ const testColumnDefs: (ColDef<TestData>)[] = [
69
+ { colId: 'id', field: 'id', headerName: 'ID', width: 100 },
70
+ { colId: 'name', field: 'name', headerName: 'Name', width: 150 },
71
+ { colId: 'value', field: 'value', headerName: 'Value', width: 100 }
72
+ ];
73
+
74
+ const testRowData: TestData[] = [
75
+ { id: 1, name: 'Item 1', value: 100 },
76
+ { id: 2, name: 'Item 2', value: 200 }
77
+ ];
78
+
79
+ beforeEach(async () => {
80
+ await TestBed.configureTestingModule({
81
+ declarations: [ArgentGridComponent],
82
+ imports: [
83
+ CommonModule,
84
+ DragDropModule
85
+ ],
86
+ providers: [
87
+ provideExperimentalZonelessChangeDetection()
88
+ ]
89
+ }).compileComponents();
90
+ });
91
+
92
+ beforeEach(() => {
93
+ fixture = TestBed.createComponent(ArgentGridComponent<TestData>) as ComponentFixture<ArgentGridComponent<TestData>>;
94
+ component = fixture.componentInstance;
95
+ component.columnDefs = testColumnDefs;
96
+ component.rowData = testRowData;
97
+
98
+ fixture.detectChanges();
99
+ });
100
+
101
+ it('should create', () => {
102
+ expect(component).toBeTruthy();
103
+ });
104
+
105
+ it('should accept columnDefs input', () => {
106
+ expect(component.columnDefs).toEqual(testColumnDefs);
107
+ });
108
+
109
+ it('should accept rowData input', () => {
110
+ expect(component.rowData).toEqual(testRowData);
111
+ });
112
+
113
+ it('should emit gridReady event', () => {
114
+ component.gridReady.subscribe((api) => {
115
+ expect(api).toBeTruthy();
116
+ expect(api.getColumnDefs()).toEqual(testColumnDefs);
117
+ });
118
+ });
119
+
120
+ it('should have correct row height', () => {
121
+ expect(component.rowHeight).toBe(32);
122
+ });
123
+
124
+ it('should have viewport for virtual scrolling', () => {
125
+ // Virtual scrolling is now handled by the viewport container
126
+ expect(component.viewportRef).toBeDefined();
127
+ });
128
+
129
+ it('should show overlay when no data', () => {
130
+ // Test the logic without calling ngOnInit which triggers canvas
131
+ expect(component.showOverlay).toBe(false); // Initially has data
132
+ });
133
+
134
+ it('should hide overlay when data exists', () => {
135
+ expect(component.showOverlay).toBe(false);
136
+ });
137
+
138
+ it('should get header name from column', () => {
139
+ const col = testColumnDefs[0];
140
+ expect(component.getHeaderName(col)).toBe('ID');
141
+ });
142
+
143
+ it('should get header name from field if headerName not provided', () => {
144
+ const col: ColDef<TestData> = { field: 'name' };
145
+ expect(component.getHeaderName(col)).toBe('name');
146
+ });
147
+
148
+ it('should handle header click for sorting', () => {
149
+ const col = { ...testColumnDefs[0], sortable: true };
150
+ component.onHeaderClick(col);
151
+ expect(col.sort).toBe('asc');
152
+
153
+ // Click again to toggle
154
+ component.onHeaderClick(col);
155
+ expect(col.sort).toBe('desc');
156
+
157
+ // Click third time to clear
158
+ component.onHeaderClick(col);
159
+ expect(col.sort).toBeNull();
160
+ });
161
+
162
+ it('should not sort non-sortable columns', () => {
163
+ const col: ColDef<TestData> = { colId: 'test', sortable: false };
164
+ component.onHeaderClick(col);
165
+ expect(col.sort).toBeUndefined();
166
+ });
167
+
168
+ it('should get column width', () => {
169
+ const col = testColumnDefs[0];
170
+ expect(component.getColumnWidth(col)).toBe(100);
171
+ });
172
+
173
+ it('should use default width if not specified', () => {
174
+ const col: ColDef<TestData> = { colId: 'test' };
175
+ expect(component.getColumnWidth(col)).toBe(150);
176
+ });
177
+
178
+ it('should refresh grid', () => {
179
+ const mockRenderer = { render: vi.fn(), destroy: vi.fn() };
180
+ (component as any).canvasRenderer = mockRenderer;
181
+ component.refresh();
182
+ expect(mockRenderer.render).toHaveBeenCalled();
183
+ });
184
+
185
+ it('should get API instance', () => {
186
+ const api = component.getApi();
187
+ expect(api).toBeTruthy();
188
+ });
189
+ });