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,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
|
+
}
|