argent-grid 0.1.0 → 0.3.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/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgentGridComponent } from './argent-grid.component';
|
|
3
|
+
|
|
4
|
+
describe('ArgentGridComponent - Regression Protection', () => {
|
|
5
|
+
let component: ArgentGridComponent<any>;
|
|
6
|
+
let mockCdr: any;
|
|
7
|
+
let mockElementRef: any;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Mock canvas context
|
|
11
|
+
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({
|
|
12
|
+
measureText: vi.fn().mockReturnValue({ width: 50 }),
|
|
13
|
+
} as any);
|
|
14
|
+
|
|
15
|
+
mockCdr = {
|
|
16
|
+
detectChanges: vi.fn(),
|
|
17
|
+
markForCheck: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
mockElementRef = {
|
|
21
|
+
nativeElement: {
|
|
22
|
+
getBoundingClientRect: () => ({ left: 100, top: 100, right: 1100, bottom: 900 }),
|
|
23
|
+
offsetWidth: 1000,
|
|
24
|
+
offsetHeight: 800,
|
|
25
|
+
style: {},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
component = new ArgentGridComponent(mockCdr, mockElementRef);
|
|
30
|
+
|
|
31
|
+
// Mock canvasRenderer
|
|
32
|
+
(component as any).canvasRenderer = {
|
|
33
|
+
getHitTestResult: vi.fn().mockReturnValue({ rowIndex: 0, columnIndex: 0 }),
|
|
34
|
+
render: vi.fn(),
|
|
35
|
+
renderFrame: vi.fn(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
component.columnDefs = [{ field: 'id', headerName: 'ID' }];
|
|
39
|
+
component.rowData = [{ id: 1 }];
|
|
40
|
+
component.ngOnInit();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should hide row group panel by default', () => {
|
|
44
|
+
expect(component.isRowGroupPanelVisible()).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should show row group panel when always is set', () => {
|
|
48
|
+
component.gridOptions = { rowGroupPanelShow: 'always' };
|
|
49
|
+
// Simulate option change
|
|
50
|
+
(component as any).onGridOptionsChanged(component.gridOptions);
|
|
51
|
+
expect(component.isRowGroupPanelVisible()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should position header menu relative to container', () => {
|
|
55
|
+
const mockEvent = {
|
|
56
|
+
stopPropagation: vi.fn(),
|
|
57
|
+
target: {
|
|
58
|
+
getBoundingClientRect: () => ({ left: 200, top: 120, right: 220, bottom: 140 }),
|
|
59
|
+
},
|
|
60
|
+
} as any;
|
|
61
|
+
|
|
62
|
+
const col = component.getApi().getAllColumns()[0];
|
|
63
|
+
component.onHeaderMenuClick(mockEvent, col);
|
|
64
|
+
|
|
65
|
+
expect(component.activeHeaderMenu).toBe(col);
|
|
66
|
+
// Container is at 100, 100. Icon is at 200, 120.
|
|
67
|
+
// Relative X = IconLeft - ContainerLeft = 200 - 100 = 100
|
|
68
|
+
// Relative Y = IconBottom - ContainerTop + 4 = 140 - 100 + 4 = 44
|
|
69
|
+
expect(component.headerMenuPosition.x).toBe(100);
|
|
70
|
+
expect(component.headerMenuPosition.y).toBe(44);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should position context menu relative to container', () => {
|
|
74
|
+
const mockEvent = {
|
|
75
|
+
preventDefault: vi.fn(),
|
|
76
|
+
stopPropagation: vi.fn(),
|
|
77
|
+
clientX: 250,
|
|
78
|
+
clientY: 300,
|
|
79
|
+
} as any;
|
|
80
|
+
|
|
81
|
+
// Mock row node finding logic
|
|
82
|
+
const rowNode = { id: '1', data: { id: 1 }, selected: false, setSelected: vi.fn() };
|
|
83
|
+
vi.spyOn(component.getApi(), 'getDisplayedRowAtIndex').mockReturnValue(rowNode as any);
|
|
84
|
+
|
|
85
|
+
component.onCanvasContextMenu(mockEvent);
|
|
86
|
+
|
|
87
|
+
// Relative X = clientX - ContainerLeft = 250 - 100 = 150
|
|
88
|
+
// Relative Y = clientY - ContainerTop = 300 - 100 = 200
|
|
89
|
+
expect(component.contextMenuPosition.x).toBe(150);
|
|
90
|
+
expect(component.contextMenuPosition.y).toBe(200);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should update rowGroupColumns when columns change', () => {
|
|
94
|
+
expect(component.rowGroupColumns.length).toBe(0);
|
|
95
|
+
|
|
96
|
+
// Add group column via API
|
|
97
|
+
component.getApi().addRowGroupColumn('id');
|
|
98
|
+
|
|
99
|
+
// The component listens to gridStateChanged$ which calls updateRowGroupColumns
|
|
100
|
+
expect(component.rowGroupColumns.length).toBe(1);
|
|
101
|
+
expect(component.rowGroupColumns[0].colId).toBe('id');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── Header Filter Button ──────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
describe('ArgentGridComponent - Header Filter Button', () => {
|
|
108
|
+
let component: ArgentGridComponent<any>;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({
|
|
112
|
+
measureText: vi.fn().mockReturnValue({ width: 50 }),
|
|
113
|
+
} as any);
|
|
114
|
+
|
|
115
|
+
const mockCdr = { detectChanges: vi.fn(), markForCheck: vi.fn() };
|
|
116
|
+
const mockElementRef = {
|
|
117
|
+
nativeElement: {
|
|
118
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
119
|
+
offsetWidth: 1000,
|
|
120
|
+
offsetHeight: 800,
|
|
121
|
+
style: {},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
component = new ArgentGridComponent(mockCdr as any, mockElementRef as any);
|
|
126
|
+
component.columnDefs = [
|
|
127
|
+
{ field: 'name', filter: 'text' },
|
|
128
|
+
{ field: 'dept', filter: 'set' },
|
|
129
|
+
{ field: 'salary', filter: 'number', floatingFilter: true },
|
|
130
|
+
{ field: 'id' }, // no filter
|
|
131
|
+
{ field: 'status', filter: 'text', suppressHeaderFilterButton: true },
|
|
132
|
+
];
|
|
133
|
+
component.rowData = [{ name: 'Alice', dept: 'Eng', salary: 100, id: 1, status: 'active' }];
|
|
134
|
+
component.ngOnInit();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('hasHeaderFilterButton', () => {
|
|
138
|
+
it('should return true for column with filter and no floatingFilter', () => {
|
|
139
|
+
const col = component
|
|
140
|
+
.getApi()
|
|
141
|
+
.getAllColumns()
|
|
142
|
+
.find((c) => c.field === 'name')!;
|
|
143
|
+
expect(component.hasHeaderFilterButton(col)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return true for set-filter column', () => {
|
|
147
|
+
const col = component
|
|
148
|
+
.getApi()
|
|
149
|
+
.getAllColumns()
|
|
150
|
+
.find((c) => c.field === 'dept')!;
|
|
151
|
+
expect(component.hasHeaderFilterButton(col)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return false when floatingFilter is enabled', () => {
|
|
155
|
+
const col = component
|
|
156
|
+
.getApi()
|
|
157
|
+
.getAllColumns()
|
|
158
|
+
.find((c) => c.field === 'salary')!;
|
|
159
|
+
expect(component.hasHeaderFilterButton(col)).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return false when no filter configured', () => {
|
|
163
|
+
const col = component
|
|
164
|
+
.getApi()
|
|
165
|
+
.getAllColumns()
|
|
166
|
+
.find((c) => c.field === 'id')!;
|
|
167
|
+
expect(component.hasHeaderFilterButton(col)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return false when suppressHeaderFilterButton is true', () => {
|
|
171
|
+
const col = component
|
|
172
|
+
.getApi()
|
|
173
|
+
.getAllColumns()
|
|
174
|
+
.find((c) => c.field === 'status')!;
|
|
175
|
+
expect(component.hasHeaderFilterButton(col)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should return false for the selection column', () => {
|
|
179
|
+
const fakeSelCol = { colId: 'ag-Grid-SelectionColumn' } as any;
|
|
180
|
+
expect(component.hasHeaderFilterButton(fakeSelCol)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('isColumnFiltered', () => {
|
|
185
|
+
it('should return false when no filter is applied', () => {
|
|
186
|
+
const col = component
|
|
187
|
+
.getApi()
|
|
188
|
+
.getAllColumns()
|
|
189
|
+
.find((c) => c.field === 'name')!;
|
|
190
|
+
expect(component.isColumnFiltered(col)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return true when a filter is active on the column', () => {
|
|
194
|
+
component
|
|
195
|
+
.getApi()
|
|
196
|
+
.setFilterModel({ name: { filterType: 'text', type: 'contains', filter: 'Alice' } });
|
|
197
|
+
const col = component
|
|
198
|
+
.getApi()
|
|
199
|
+
.getAllColumns()
|
|
200
|
+
.find((c) => c.field === 'name')!;
|
|
201
|
+
expect(component.isColumnFiltered(col)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return false for a different column when only one filtered', () => {
|
|
205
|
+
component
|
|
206
|
+
.getApi()
|
|
207
|
+
.setFilterModel({ name: { filterType: 'text', type: 'contains', filter: 'Alice' } });
|
|
208
|
+
const col = component
|
|
209
|
+
.getApi()
|
|
210
|
+
.getAllColumns()
|
|
211
|
+
.find((c) => c.field === 'dept')!;
|
|
212
|
+
expect(component.isColumnFiltered(col)).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('openColumnsPanel', () => {
|
|
217
|
+
it('should make sideBarVisible true', () => {
|
|
218
|
+
component.sideBarVisible = false;
|
|
219
|
+
component.openColumnsPanel();
|
|
220
|
+
expect(component.sideBarVisible).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should set activeToolPanel to columns', () => {
|
|
224
|
+
component.activeToolPanel = null;
|
|
225
|
+
component.openColumnsPanel();
|
|
226
|
+
expect(component.activeToolPanel).toBe('columns');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should close any open header menu', () => {
|
|
230
|
+
const col = component.getApi().getAllColumns()[0];
|
|
231
|
+
component.activeHeaderMenu = col;
|
|
232
|
+
component.openColumnsPanel();
|
|
233
|
+
expect(component.activeHeaderMenu).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ─── openSetFilter restores selected values ─────────────────────────────────
|
|
239
|
+
|
|
240
|
+
describe('ArgentGridComponent - openSetFilter restores filter state', () => {
|
|
241
|
+
let component: ArgentGridComponent<any>;
|
|
242
|
+
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({
|
|
245
|
+
measureText: vi.fn().mockReturnValue({ width: 50 }),
|
|
246
|
+
} as any);
|
|
247
|
+
|
|
248
|
+
const mockCdr = { detectChanges: vi.fn(), markForCheck: vi.fn() };
|
|
249
|
+
const mockElementRef = {
|
|
250
|
+
nativeElement: {
|
|
251
|
+
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
|
252
|
+
offsetWidth: 1000,
|
|
253
|
+
offsetHeight: 800,
|
|
254
|
+
style: {},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
component = new ArgentGridComponent(mockCdr as any, mockElementRef as any);
|
|
259
|
+
component.columnDefs = [{ field: 'dept', filter: 'set' }];
|
|
260
|
+
component.rowData = [{ dept: 'Engineering' }, { dept: 'Sales' }, { dept: 'Marketing' }];
|
|
261
|
+
component.ngOnInit();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should set setFilterSelectedValues to null when no existing filter', () => {
|
|
265
|
+
const col = component.getApi().getAllColumns()[0];
|
|
266
|
+
component.openSetFilter(null, col, { x: 0, y: 0 });
|
|
267
|
+
expect(component.setFilterSelectedValues).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should restore previously selected values from the filter model', () => {
|
|
271
|
+
component.getApi().setFilterModel({
|
|
272
|
+
dept: { filterType: 'set', values: ['Engineering', 'Sales'] },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const col = component.getApi().getAllColumns()[0];
|
|
276
|
+
component.openSetFilter(null, col, { x: 0, y: 0 });
|
|
277
|
+
|
|
278
|
+
expect(component.setFilterSelectedValues).toEqual(['Engineering', 'Sales']);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should set setFilterSelectedValues to null for non-set filter models', () => {
|
|
282
|
+
// Manually set a text filter (wrong type) under the dept colId
|
|
283
|
+
component.getApi().setFilterModel({
|
|
284
|
+
dept: { filterType: 'text', type: 'contains', filter: 'Eng' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const col = component.getApi().getAllColumns()[0];
|
|
288
|
+
component.openSetFilter(null, col, { x: 0, y: 0 });
|
|
289
|
+
|
|
290
|
+
expect(component.setFilterSelectedValues).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should populate setFilterValues with all unique values for the column', () => {
|
|
294
|
+
const col = component.getApi().getAllColumns()[0];
|
|
295
|
+
component.openSetFilter(null, col, { x: 0, y: 0 });
|
|
296
|
+
|
|
297
|
+
expect(component.setFilterValues).toContain('Engineering');
|
|
298
|
+
expect(component.setFilterValues).toContain('Sales');
|
|
299
|
+
expect(component.setFilterValues).toContain('Marketing');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { ArgentGridModule } from '../argent-grid.module';
|
|
5
|
+
import { GridService } from '../services/grid.service';
|
|
6
|
+
import { ArgentGridComponent } from './argent-grid.component';
|
|
7
|
+
|
|
8
|
+
interface TestData {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('ArgentGridComponent - Selection Behavior', () => {
|
|
14
|
+
let component: ArgentGridComponent<TestData>;
|
|
15
|
+
let fixture: ComponentFixture<ArgentGridComponent<TestData>>;
|
|
16
|
+
|
|
17
|
+
const testColumnDefs = [
|
|
18
|
+
{ field: 'id', headerName: 'ID', checkboxSelection: true },
|
|
19
|
+
{ field: 'name', headerName: 'Name' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const testRowData = [
|
|
23
|
+
{ id: 1, name: 'Item 1' },
|
|
24
|
+
{ id: 2, name: 'Item 2' },
|
|
25
|
+
{ id: 3, name: 'Item 3' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
await TestBed.configureTestingModule({
|
|
30
|
+
imports: [ArgentGridModule],
|
|
31
|
+
providers: [GridService, provideExperimentalZonelessChangeDetection()],
|
|
32
|
+
}).compileComponents();
|
|
33
|
+
|
|
34
|
+
fixture = TestBed.createComponent(ArgentGridComponent);
|
|
35
|
+
component = fixture.componentInstance;
|
|
36
|
+
component.columnDefs = testColumnDefs;
|
|
37
|
+
component.rowData = testRowData;
|
|
38
|
+
component.rowSelection = 'multiple';
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should initialize with no rows selected', () => {
|
|
43
|
+
expect(component.isAllSelected).toBe(false);
|
|
44
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
45
|
+
expect(component.getApi().getSelectedRows().length).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should toggle all rows when header checkbox is clicked', () => {
|
|
49
|
+
// Select all
|
|
50
|
+
const event = { target: { checked: true } } as any;
|
|
51
|
+
component.onSelectionHeaderChange(event);
|
|
52
|
+
|
|
53
|
+
expect(component.getApi().getSelectedRows().length).toBe(3);
|
|
54
|
+
expect(component.isAllSelected).toBe(true);
|
|
55
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
56
|
+
|
|
57
|
+
// Deselect all
|
|
58
|
+
event.target.checked = false;
|
|
59
|
+
component.onSelectionHeaderChange(event);
|
|
60
|
+
|
|
61
|
+
expect(component.getApi().getSelectedRows().length).toBe(0);
|
|
62
|
+
expect(component.isAllSelected).toBe(false);
|
|
63
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should update indeterminate state when single row is selected', () => {
|
|
67
|
+
const api = component.getApi();
|
|
68
|
+
const node = api.getDisplayedRowAtIndex(0);
|
|
69
|
+
|
|
70
|
+
node?.setSelected(true);
|
|
71
|
+
// GridState listener in component should trigger updateSelectionState
|
|
72
|
+
// We might need to manually trigger or wait for microtasks if not using zone.js
|
|
73
|
+
component.updateSelectionState();
|
|
74
|
+
|
|
75
|
+
expect(component.getApi().getSelectedRows().length).toBe(1);
|
|
76
|
+
expect(component.isAllSelected).toBe(false);
|
|
77
|
+
expect(component.isIndeterminateSelection).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should toggle selection when a row is clicked', () => {
|
|
81
|
+
const api = component.getApi();
|
|
82
|
+
const mouseEvent = new MouseEvent('click');
|
|
83
|
+
|
|
84
|
+
// Select row
|
|
85
|
+
component.onRowClick(0, mouseEvent);
|
|
86
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
87
|
+
|
|
88
|
+
// Unselect row by clicking again (toggle behavior)
|
|
89
|
+
component.onRowClick(0, mouseEvent);
|
|
90
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should clear others when clicking a row without modifiers', () => {
|
|
94
|
+
const api = component.getApi();
|
|
95
|
+
const mouseEvent = new MouseEvent('click');
|
|
96
|
+
|
|
97
|
+
// Select first row
|
|
98
|
+
component.onRowClick(0, mouseEvent);
|
|
99
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
100
|
+
|
|
101
|
+
// Click second row
|
|
102
|
+
component.onRowClick(1, mouseEvent);
|
|
103
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(false);
|
|
104
|
+
expect(api.getDisplayedRowAtIndex(1)?.selected).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should keep multiple selection when using Ctrl key', () => {
|
|
108
|
+
const api = component.getApi();
|
|
109
|
+
const ctrlEvent = new MouseEvent('click', { ctrlKey: true });
|
|
110
|
+
|
|
111
|
+
component.onRowClick(0, ctrlEvent);
|
|
112
|
+
component.onRowClick(1, ctrlEvent);
|
|
113
|
+
|
|
114
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
115
|
+
expect(api.getDisplayedRowAtIndex(1)?.selected).toBe(true);
|
|
116
|
+
expect(api.getSelectedRows().length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should disable sorting and menu for dedicated selection column', () => {
|
|
120
|
+
const selectionCol = component.getApi().getAllColumns()[0];
|
|
121
|
+
expect(selectionCol.colId).toBe('ag-Grid-SelectionColumn');
|
|
122
|
+
|
|
123
|
+
expect(component.isSortable(selectionCol)).toBe(false);
|
|
124
|
+
expect(component.hasHeaderMenu(selectionCol)).toBe(false);
|
|
125
|
+
expect(component.getHeaderName(selectionCol)).toBe('');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should allow resizing for dedicated selection column', () => {
|
|
129
|
+
const selectionCol = component.getApi().getAllColumns()[0];
|
|
130
|
+
expect(component.isResizable(selectionCol)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { SetFilterComponent } from './set-filter.component';
|
|
3
|
+
|
|
4
|
+
describe('SetFilterComponent', () => {
|
|
5
|
+
let component: SetFilterComponent<string>;
|
|
6
|
+
|
|
7
|
+
const values = ['Engineering', 'Sales', 'Marketing', 'Engineering', 'Sales'];
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
component = new SetFilterComponent();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ── initialSelectedValues ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('initialSelectedValues input', () => {
|
|
16
|
+
it('should check all items when initialSelectedValues is null (default)', () => {
|
|
17
|
+
component.values = values;
|
|
18
|
+
component.initialSelectedValues = null;
|
|
19
|
+
component.ngOnInit();
|
|
20
|
+
|
|
21
|
+
expect(component.allValues.every((v) => v.selected)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should pre-check only the provided values when initialSelectedValues is set', () => {
|
|
25
|
+
component.values = values;
|
|
26
|
+
component.initialSelectedValues = ['Engineering'];
|
|
27
|
+
component.ngOnInit();
|
|
28
|
+
|
|
29
|
+
const eng = component.allValues.find((v) => v.value === 'Engineering');
|
|
30
|
+
const sales = component.allValues.find((v) => v.value === 'Sales');
|
|
31
|
+
const mkt = component.allValues.find((v) => v.value === 'Marketing');
|
|
32
|
+
|
|
33
|
+
expect(eng?.selected).toBe(true);
|
|
34
|
+
expect(sales?.selected).toBe(false);
|
|
35
|
+
expect(mkt?.selected).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should populate selectedValues matching initialSelectedValues', () => {
|
|
39
|
+
component.values = values;
|
|
40
|
+
component.initialSelectedValues = ['Sales', 'Marketing'];
|
|
41
|
+
component.ngOnInit();
|
|
42
|
+
|
|
43
|
+
expect(component.selectedValues).toEqual(expect.arrayContaining(['Sales', 'Marketing']));
|
|
44
|
+
expect(component.selectedValues).not.toContain('Engineering');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle empty initialSelectedValues (none checked)', () => {
|
|
48
|
+
component.values = values;
|
|
49
|
+
component.initialSelectedValues = [];
|
|
50
|
+
component.ngOnInit();
|
|
51
|
+
|
|
52
|
+
expect(component.allValues.every((v) => !v.selected)).toBe(true);
|
|
53
|
+
expect(component.selectedValues).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle initialSelectedValues with values not present in list gracefully', () => {
|
|
57
|
+
component.values = values;
|
|
58
|
+
component.initialSelectedValues = ['NonExistent'];
|
|
59
|
+
component.ngOnInit();
|
|
60
|
+
|
|
61
|
+
expect(component.allValues.every((v) => !v.selected)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── resetFilter ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe('resetFilter', () => {
|
|
68
|
+
it('should re-select all items after reset, regardless of initialSelectedValues', () => {
|
|
69
|
+
component.values = values;
|
|
70
|
+
component.initialSelectedValues = ['Engineering'];
|
|
71
|
+
component.ngOnInit();
|
|
72
|
+
|
|
73
|
+
// Confirm only Engineering is selected before reset
|
|
74
|
+
expect(component.allValues.filter((v) => v.selected).length).toBe(1);
|
|
75
|
+
|
|
76
|
+
const emit = vi.fn();
|
|
77
|
+
component.filterChanged.emit = emit;
|
|
78
|
+
component.resetFilter();
|
|
79
|
+
|
|
80
|
+
// After reset, all items should be selected
|
|
81
|
+
expect(component.allValues.every((v) => v.selected)).toBe(true);
|
|
82
|
+
expect(emit).toHaveBeenCalledWith(
|
|
83
|
+
expect.arrayContaining(['Engineering', 'Sales', 'Marketing'])
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should clear search text on reset', () => {
|
|
88
|
+
component.values = values;
|
|
89
|
+
component.ngOnInit();
|
|
90
|
+
component.searchText = 'Eng';
|
|
91
|
+
|
|
92
|
+
component.resetFilter();
|
|
93
|
+
|
|
94
|
+
expect(component.searchText).toBe('');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── selectAll / clearAll ───────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe('selectAll', () => {
|
|
101
|
+
it('should mark all items selected', () => {
|
|
102
|
+
component.values = values;
|
|
103
|
+
component.initialSelectedValues = ['Engineering'];
|
|
104
|
+
component.ngOnInit();
|
|
105
|
+
|
|
106
|
+
component.selectAll();
|
|
107
|
+
|
|
108
|
+
expect(component.allValues.every((v) => v.selected)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('clearAll', () => {
|
|
113
|
+
it('should uncheck all items', () => {
|
|
114
|
+
component.values = values;
|
|
115
|
+
component.ngOnInit();
|
|
116
|
+
|
|
117
|
+
component.clearAll();
|
|
118
|
+
|
|
119
|
+
expect(component.allValues.every((v) => !v.selected)).toBe(true);
|
|
120
|
+
expect(component.selectedValues).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── applyFilter ────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe('applyFilter', () => {
|
|
127
|
+
it('should emit only selected values', () => {
|
|
128
|
+
component.values = values;
|
|
129
|
+
component.initialSelectedValues = ['Engineering'];
|
|
130
|
+
component.ngOnInit();
|
|
131
|
+
|
|
132
|
+
const emit = vi.fn();
|
|
133
|
+
component.filterChanged.emit = emit;
|
|
134
|
+
component.applyFilter();
|
|
135
|
+
|
|
136
|
+
expect(emit).toHaveBeenCalledWith(['Engineering']);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── filteredValues (search) ────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe('filteredValues', () => {
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
component.values = values;
|
|
145
|
+
component.ngOnInit();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return all values when search is empty', () => {
|
|
149
|
+
expect(component.filteredValues.length).toBe(3); // unique: Eng, Sales, Mkt
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should filter by search text (case-insensitive)', () => {
|
|
153
|
+
component.searchText = 'eng';
|
|
154
|
+
expect(component.filteredValues.length).toBe(1);
|
|
155
|
+
expect(component.filteredValues[0].value).toBe('Engineering');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return empty when search matches nothing', () => {
|
|
159
|
+
component.searchText = 'zzz';
|
|
160
|
+
expect(component.filteredValues.length).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── value counts ──────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
describe('value counts', () => {
|
|
167
|
+
it('should count occurrences correctly', () => {
|
|
168
|
+
component.values = values; // Eng x2, Sales x2, Mkt x1
|
|
169
|
+
component.ngOnInit();
|
|
170
|
+
|
|
171
|
+
const eng = component.allValues.find((v) => v.value === 'Engineering');
|
|
172
|
+
const mkt = component.allValues.find((v) => v.value === 'Marketing');
|
|
173
|
+
|
|
174
|
+
expect(eng?.count).toBe(2);
|
|
175
|
+
expect(mkt?.count).toBe(1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── valueFormatter ────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
describe('valueFormatter', () => {
|
|
182
|
+
it('should apply formatter to display values', () => {
|
|
183
|
+
component.values = ['eng', 'sales'];
|
|
184
|
+
component.valueFormatter = (v) => v.toUpperCase();
|
|
185
|
+
component.ngOnInit();
|
|
186
|
+
|
|
187
|
+
expect(component.allValues[0].displayValue).toBe('ENG');
|
|
188
|
+
expect(component.allValues[1].displayValue).toBe('SALES');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|