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,77 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('ArgentGrid Demo', () => {
4
+ test('should load the demo page without errors', async ({ page }) => {
5
+ // Set up console error listener to catch Angular errors
6
+ const errors: string[] = [];
7
+ const consoleMessages: string[] = [];
8
+
9
+ page.on('console', (msg) => {
10
+ const text = msg.text();
11
+ consoleMessages.push(text);
12
+ if (msg.type() === 'error') {
13
+ errors.push(text);
14
+ console.log('Console error:', text);
15
+ }
16
+ });
17
+
18
+ page.on('pageerror', (error) => {
19
+ errors.push(error.message);
20
+ console.log('Page error:', error.message);
21
+ });
22
+
23
+ // Navigate to the demo page
24
+ await page.goto('/', { waitUntil: 'networkidle', timeout: 30000 });
25
+
26
+ // Take a screenshot for debugging
27
+ await page.screenshot({ path: 'e2e/screenshots/page-load.png' });
28
+
29
+ // Get page content for debugging
30
+ const content = await page.content();
31
+ console.log('Page content length:', content.length);
32
+
33
+ // Check for Angular errors in console
34
+ const ngErrors = errors.filter((err) => err.includes('NG0') || err.includes('ERROR'));
35
+
36
+ if (ngErrors.length > 0) {
37
+ console.error('Angular errors detected:', ngErrors);
38
+ }
39
+
40
+ // Wait for the page to load - check for app-root
41
+ await page.waitForSelector('app-root', { timeout: 10000 });
42
+
43
+ // Wait for demo container
44
+ await page.waitForSelector('.demo-container', { timeout: 10000 });
45
+
46
+ // Assert no critical Angular injection errors
47
+ const criticalErrors = ngErrors.filter(err =>
48
+ err.includes('NG0203') || err.includes('NG0201') || err.includes('NullInjectorError')
49
+ );
50
+
51
+ expect(criticalErrors).toHaveLength(0);
52
+
53
+ // Check that the grid container is visible
54
+ const gridContainer = page.locator('.argent-grid-container');
55
+ await expect(gridContainer).toBeVisible({ timeout: 10000 });
56
+
57
+ // Check that rows are loaded
58
+ await expect(page.locator('.stat-badge').first()).toBeVisible();
59
+ });
60
+
61
+ test('should load 100K rows successfully', async ({ page }) => {
62
+ await page.goto('/', { waitUntil: 'networkidle' });
63
+
64
+ // Wait for initial load
65
+ await page.waitForSelector('argent-grid', { timeout: 10000 });
66
+
67
+ // Click the 100K button
68
+ await page.click('button:has-text("100K")');
69
+
70
+ // Wait for loading to complete
71
+ await page.waitForSelector('.loading', { state: 'detached', timeout: 30000 });
72
+
73
+ // Verify row count updated
74
+ const stats = await page.locator('.stat-badge').first().textContent();
75
+ expect(stats).toContain('100,000');
76
+ });
77
+ });
@@ -0,0 +1,269 @@
1
+ import { test, expect, Page } from '@playwright/test';
2
+
3
+ test.describe('ArgentGrid Feature Guard Rails', () => {
4
+ test.beforeEach(async ({ page }) => {
5
+ await page.goto('/', { waitUntil: 'networkidle' });
6
+ // Wait for grid to be ready
7
+ await page.waitForSelector('argent-grid', { timeout: 10000 });
8
+ // Wait for data to load
9
+ await page.waitForSelector('.loading', { state: 'detached', timeout: 30000 });
10
+ });
11
+
12
+ test('should support row grouping and expansion', async ({ page }) => {
13
+ // 1. Toggle Grouping
14
+ await page.click('button:has-text("Group by Dept")');
15
+
16
+ // 2. Verify "Organization" column (Auto Group Column) appears
17
+ const groupHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^Organization/ });
18
+ await expect(groupHeader).toBeVisible();
19
+
20
+ // 3. Verify original "Department" column is hidden
21
+ const deptHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^Department/ });
22
+ await expect(deptHeader).not.toBeVisible();
23
+
24
+ // 4. Verify row count via GridApi (initially grouped)
25
+ const rowCountBefore = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
26
+ expect(rowCountBefore).toBeLessThan(20);
27
+
28
+ // 5. Expand first group (click near chevron)
29
+ const canvas = page.locator('canvas');
30
+ const box = await canvas.boundingBox();
31
+ if (!box) throw new Error('Canvas box not found');
32
+
33
+ // Click at x=20, y=16 (roughly the first group's chevron)
34
+ await page.mouse.click(box.x + 20, box.y + 16);
35
+
36
+ // 6. Verify row count increased
37
+ await page.waitForTimeout(1000); // Wait for processing
38
+ const rowCountAfter = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
39
+ expect(rowCountAfter).toBeGreaterThan(rowCountBefore);
40
+ });
41
+
42
+ test('should support floating filters', async ({ page }) => {
43
+ // 1. Verify floating filter row is visible
44
+ const filterRow = page.locator('.floating-filter-row');
45
+ await expect(filterRow).toBeVisible();
46
+
47
+ // 2. Type "Sales" into the Department filter (3rd input)
48
+ const deptFilter = page.locator('.floating-filter-input').nth(2);
49
+ await deptFilter.fill('Sales');
50
+
51
+ // 3. Wait for debounce and verify filtered count
52
+ await page.waitForTimeout(1500);
53
+ const filteredCount = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
54
+
55
+ expect(filteredCount).toBeGreaterThan(0);
56
+ expect(filteredCount).toBeLessThan(100000);
57
+
58
+ // 4. Verify Clear button (x) works
59
+ const clearBtn = page.locator('.floating-filter-clear').first();
60
+ await expect(clearBtn).toBeVisible();
61
+ await clearBtn.click();
62
+
63
+ // 5. Verify count restored
64
+ await page.waitForTimeout(1000);
65
+ const restoredCount = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
66
+ expect(restoredCount).toBe(100000);
67
+ });
68
+
69
+ test('should support cell editing with keyboard navigation', async ({ page }) => {
70
+ const canvas = page.locator('canvas');
71
+ const box = await canvas.boundingBox();
72
+ if (!box) throw new Error('Canvas box not found');
73
+
74
+ // 1. Double click to start editing (Name column, first row)
75
+ // Click on Name column (x=150 is roughly middle of Name column)
76
+ await canvas.dblclick({ position: { x: 150, y: 16 } });
77
+
78
+ const editor = page.locator('.argent-grid-editor-input');
79
+ await expect(editor).toBeVisible();
80
+
81
+ // 2. Type new value and Enter to save
82
+ await editor.fill('TEST_EDIT_SUCCESS');
83
+ await page.keyboard.press('Enter');
84
+
85
+ // 3. Verify editor is gone and value persisted
86
+ await expect(editor).not.toBeVisible();
87
+ const savedValue = await page.evaluate(() => window.gridApi.getDisplayedRowAtIndex(0).data.name);
88
+ expect(savedValue).toBe('TEST_EDIT_SUCCESS');
89
+
90
+ // 4. Test Escape to cancel
91
+ await canvas.dblclick({ position: { x: 150, y: 16 } });
92
+ await expect(editor).toBeVisible();
93
+ await editor.fill('WILL_CANCEL');
94
+ await page.keyboard.press('Escape');
95
+
96
+ await expect(editor).not.toBeVisible();
97
+ const afterCancelValue = await page.evaluate(() => window.gridApi.getDisplayedRowAtIndex(0).data.name);
98
+ expect(afterCancelValue).toBe('TEST_EDIT_SUCCESS');
99
+ });
100
+
101
+ test('should support column pinning and horizontal scroll sync', async ({ page }) => {
102
+ // Force horizontal scroll by reducing viewport
103
+ await page.setViewportSize({ width: 800, height: 600 });
104
+
105
+ // 1. Pin ID column to Left via Menu
106
+ const idHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^ID/ });
107
+ await idHeader.hover();
108
+ const menuIcon = idHeader.locator('.argent-grid-header-menu-icon');
109
+ await menuIcon.waitFor({ state: 'visible' });
110
+ await menuIcon.click();
111
+
112
+ // Click Pin Left
113
+ await page.locator('.menu-item').filter({ hasText: 'Pin Left' }).click();
114
+
115
+ // 2. Verify internal state via GridApi
116
+ await page.waitForFunction(() => {
117
+ const api = (window as any).gridApi;
118
+ const col = api.getAllColumns().find(c => c.colId === 'id');
119
+ return col && col.pinned === 'left';
120
+ }, { timeout: 5000 });
121
+
122
+ // 3. Scroll grid horizontally
123
+ const viewport = page.locator('.argent-grid-viewport');
124
+ await viewport.evaluate(el => el.scrollLeft = 200);
125
+
126
+ // 4. Verify header scroll sync
127
+ await page.waitForTimeout(1000);
128
+ const headerScrollable = page.locator('.argent-grid-header-scrollable').first();
129
+ const headerScrollLeft = await headerScrollable.evaluate(el => el.scrollLeft);
130
+ expect(headerScrollLeft).toBe(200);
131
+
132
+ // 5. Verify pinned column remains at the left
133
+ const idHeaderBox = await idHeader.boundingBox();
134
+ const containerBox = await page.locator('.argent-grid-container').boundingBox();
135
+ // Allow 2px margin for borders
136
+ expect(Math.abs((idHeaderBox?.x || 0) - (containerBox?.x || 0))).toBeLessThanOrEqual(2);
137
+ });
138
+
139
+ test('should support column re-ordering via drag and drop', async ({ page }) => {
140
+ const idHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^ID/ });
141
+ const nameHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^Name/ });
142
+
143
+ const idBoxBefore = await idHeader.boundingBox();
144
+ const nameBoxBefore = await nameHeader.boundingBox();
145
+
146
+ expect(idBoxBefore!.x).toBeLessThan(nameBoxBefore!.x);
147
+
148
+ // Drag Name to before ID
149
+ await nameHeader.hover();
150
+ await page.mouse.down();
151
+ await page.mouse.move(idBoxBefore!.x + 10, idBoxBefore!.y + 10, { steps: 10 });
152
+ await page.mouse.up();
153
+
154
+ // Verify order swapped
155
+ await page.waitForTimeout(1000);
156
+ const idBoxAfter = await idHeader.boundingBox();
157
+ const nameBoxAfter = await nameHeader.boundingBox();
158
+
159
+ expect(nameBoxAfter!.x).toBeLessThan(idBoxAfter!.x);
160
+ });
161
+
162
+ test('should support programmatic filter synchronization and UI reactivity', async ({ page }) => {
163
+ // 1. Apply filter via "Filter Engineering" button (calls gridApi.setFilterModel)
164
+ await page.click('button:has-text("Filter \'Engineering\'")');
165
+
166
+ // 2. Verify row count via GridApi dropped (reactivity check)
167
+ await page.waitForTimeout(1000);
168
+ const filteredCount = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
169
+ expect(filteredCount).toBeLessThan(100000);
170
+ expect(filteredCount).toBeGreaterThan(0);
171
+
172
+ // 3. Verify the floating filter input shows "Eng" (bidirectional sync check)
173
+ const deptFilter = page.locator('.floating-filter-input').nth(2);
174
+ const filterValue = await deptFilter.inputValue();
175
+ expect(filterValue).toBe('Eng');
176
+
177
+ // 4. Clear via "Clear Filters" button
178
+ await page.click('button:has-text("Clear Filters")');
179
+ await page.waitForTimeout(500);
180
+
181
+ // 5. Verify input cleared and count restored
182
+ const clearedValue = await deptFilter.inputValue();
183
+ expect(clearedValue).toBe('');
184
+ const finalCount = await page.evaluate(() => window.gridApi.getDisplayedRowCount());
185
+ expect(finalCount).toBe(100000);
186
+ });
187
+
188
+ test('should support global options toggle via setGridOption', async ({ page }) => {
189
+ // 1. Hide Filters via button (calls gridApi.setGridOption)
190
+ await page.click('button:has-text("Hide Filters")');
191
+
192
+ // 2. Verify floating filter row is removed from DOM
193
+ const filterRow = page.locator('.floating-filter-row');
194
+ await expect(filterRow).not.toBeVisible();
195
+
196
+ // 3. Show Filters back
197
+ await page.click('button:has-text("Show Filters")');
198
+
199
+ // 4. Verify floating filter row returned
200
+ await expect(filterRow).toBeVisible();
201
+ });
202
+
203
+ test('should support column resizing by dragging the handle', async ({ page }) => {
204
+ const idHeader = page.locator('.argent-grid-header-cell').filter({ hasText: /^ID/ });
205
+ const idBoxBefore = await idHeader.boundingBox();
206
+ if (!idBoxBefore) throw new Error('ID header box not found');
207
+
208
+ const initialWidth = idBoxBefore.width;
209
+
210
+ // Locate resize handle
211
+ const resizeHandle = idHeader.locator('.argent-grid-header-resize-handle');
212
+ await expect(resizeHandle).toBeVisible();
213
+
214
+ // Drag to resize
215
+ await resizeHandle.hover();
216
+ await page.mouse.down();
217
+ await page.mouse.move(idBoxBefore.x + idBoxBefore.width + 100, idBoxBefore.y + 10, { steps: 10 });
218
+ await page.mouse.up();
219
+
220
+ // Verify width increased
221
+ await page.waitForTimeout(500);
222
+ const idBoxAfter = await idHeader.boundingBox();
223
+ expect(idBoxAfter!.width).toBeGreaterThan(initialWidth);
224
+ // Should be roughly initialWidth + 100 (allow 10px margin)
225
+ expect(Math.abs(idBoxAfter!.width - (initialWidth + 100))).toBeLessThanOrEqual(10);
226
+ });
227
+
228
+ test('should support Excel-like range selection by dragging', async ({ page }) => {
229
+ const canvas = page.locator('canvas');
230
+ const box = await canvas.boundingBox();
231
+ if (!box) throw new Error('Canvas box not found');
232
+
233
+ // 1. Start drag at cell (row 1, col 'name')
234
+ // Name column starts at x=80 (if selection col hidden) or x=112 (if selection col shown)
235
+ // selectionColumnWidth is 32.
236
+ // ID column is 80.
237
+ // Name column starts around x = 32 + 80 = 112.
238
+ const startX = 150;
239
+ const startY = 16; // Middle of first row (32px high)
240
+
241
+ await page.mouse.move(box.x + startX, box.y + startY);
242
+ await page.mouse.down();
243
+
244
+ // 2. Drag to cell (row 3, col 'department')
245
+ // Row 3 is at y = 32 * 2 + 16 = 80.
246
+ // Dept column is 180 wide.
247
+ const endX = 350;
248
+ const endY = 80;
249
+
250
+ await page.mouse.move(box.x + endX, box.y + endY, { steps: 10 });
251
+ await page.mouse.up();
252
+
253
+ // 3. Verify range selection via API
254
+ const ranges = await page.evaluate(() => window.gridApi.getCellRanges());
255
+ expect(ranges).toBeTruthy();
256
+ expect(ranges.length).toBe(1);
257
+
258
+ const range = ranges[0];
259
+ expect(range.startRow).toBe(0);
260
+ expect(range.endRow).toBe(2); // row 1 to 3
261
+ expect(range.columns.length).toBeGreaterThan(1);
262
+ });
263
+ });
264
+
265
+ declare global {
266
+ interface Window {
267
+ gridApi: any;
268
+ }
269
+ }