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.
Files changed (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. 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
+ });