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,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
|
+
⋮
|
|
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
|
+
⋮
|
|
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
|
+
⋮
|
|
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
|
+
});
|