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,453 @@
1
+ /**
2
+ * Unit tests for Blitting Optimization
3
+ *
4
+ * Tests for blit calculations, execution, and state management.
5
+ */
6
+
7
+ import {
8
+ shouldBlit,
9
+ calculateBlit,
10
+ blitLastFrame,
11
+ createBufferPair,
12
+ swapBuffers,
13
+ displayBuffer,
14
+ resizeBufferPair,
15
+ BlitState,
16
+ MIN_BLIT_DELTA,
17
+ MAX_BLIT_DELTA_RATIO,
18
+ } from './blit';
19
+ import { Rectangle, BufferPair } from './types';
20
+
21
+ // Mock canvas and context
22
+ const mockContext = {
23
+ canvas: { width: 800, height: 600, style: {} } as any,
24
+ drawImage: vi.fn(),
25
+ setTransform: vi.fn(),
26
+ getImageData: vi.fn(() => ({ data: new Uint8ClampedArray([255, 0, 0, 255]) })),
27
+ fillRect: vi.fn(),
28
+ fillStyle: '',
29
+ } as unknown as CanvasRenderingContext2D;
30
+
31
+ // Mock HTMLCanvasElement
32
+ class MockCanvas {
33
+ width = 800;
34
+ height = 600;
35
+ style = { width: '800px', height: '600px' };
36
+
37
+ getContext() {
38
+ return mockContext;
39
+ }
40
+ }
41
+
42
+ // Mock document.createElement for canvas
43
+ const originalCreateElement = document.createElement.bind(document);
44
+ beforeAll(() => {
45
+ vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
46
+ if (tagName.toLowerCase() === 'canvas') {
47
+ return new MockCanvas() as unknown as HTMLCanvasElement;
48
+ }
49
+ return originalCreateElement(tagName);
50
+ });
51
+ });
52
+
53
+ afterAll(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ describe('Blitting Optimization', () => {
58
+ describe('shouldBlit', () => {
59
+ it('should return false for no scroll', () => {
60
+ expect(shouldBlit(0, 0, 800, 600)).toBe(false);
61
+ });
62
+
63
+ it('should return false for tiny scroll deltas', () => {
64
+ expect(shouldBlit(1, 0, 800, 600)).toBe(false);
65
+ expect(shouldBlit(0, 1, 800, 600)).toBe(false);
66
+ });
67
+
68
+ it('should return true for moderate vertical scroll', () => {
69
+ expect(shouldBlit(0, 10, 800, 600)).toBe(true);
70
+ });
71
+
72
+ it('should return true for moderate horizontal scroll', () => {
73
+ expect(shouldBlit(10, 0, 800, 600)).toBe(true);
74
+ });
75
+
76
+ it('should return false for diagonal scroll', () => {
77
+ expect(shouldBlit(10, 10, 800, 600)).toBe(false);
78
+ });
79
+
80
+ it('should return false for large scroll delta', () => {
81
+ // More than 80% of viewport
82
+ expect(shouldBlit(0, 500, 800, 600)).toBe(false);
83
+ expect(shouldBlit(650, 0, 800, 600)).toBe(false);
84
+ });
85
+
86
+ it('should use MIN_BLIT_DELTA threshold', () => {
87
+ expect(shouldBlit(MIN_BLIT_DELTA - 1, 0, 800, 600)).toBe(false);
88
+ expect(shouldBlit(MIN_BLIT_DELTA, 0, 800, 600)).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe('calculateBlit', () => {
93
+ const viewportSize = { width: 800, height: 600 };
94
+ const pinnedWidths = { left: 100, right: 50 };
95
+
96
+ it('should return canBlit=false for no scroll change', () => {
97
+ const result = calculateBlit(
98
+ { x: 100, y: 100 },
99
+ { x: 100, y: 100 },
100
+ viewportSize,
101
+ pinnedWidths
102
+ );
103
+
104
+ expect(result.canBlit).toBe(false);
105
+ expect(result.dirtyRegions).toHaveLength(1);
106
+ expect(result.dirtyRegions[0]).toEqual({
107
+ x: 0,
108
+ y: 0,
109
+ width: 800,
110
+ height: 600,
111
+ });
112
+ });
113
+
114
+ it('should calculate vertical scroll blit (down)', () => {
115
+ const result = calculateBlit(
116
+ { x: 0, y: 100 }, // Current
117
+ { x: 0, y: 0 }, // Last
118
+ viewportSize,
119
+ pinnedWidths
120
+ );
121
+
122
+ expect(result.canBlit).toBe(true);
123
+ expect(result.deltaY).toBe(100);
124
+ expect(result.deltaX).toBe(0);
125
+
126
+ // Scrolling down: top strip needs redraw
127
+ expect(result.dirtyRegions).toHaveLength(1);
128
+ expect(result.dirtyRegions[0].y).toBe(0);
129
+ expect(result.dirtyRegions[0].height).toBe(100);
130
+
131
+ // Source should start at 0
132
+ expect(result.sourceRect.y).toBe(0);
133
+ // Dest should start at deltaY
134
+ expect(result.destRect.y).toBe(100);
135
+ });
136
+
137
+ it('should calculate vertical scroll blit (up)', () => {
138
+ const result = calculateBlit(
139
+ { x: 0, y: 0 }, // Current
140
+ { x: 0, y: 100 }, // Last
141
+ viewportSize,
142
+ pinnedWidths
143
+ );
144
+
145
+ expect(result.canBlit).toBe(true);
146
+ expect(result.deltaY).toBe(-100);
147
+
148
+ // Scrolling up: bottom strip needs redraw
149
+ expect(result.dirtyRegions).toHaveLength(1);
150
+ expect(result.dirtyRegions[0].y).toBe(500); // 600 - 100
151
+ expect(result.dirtyRegions[0].height).toBe(100);
152
+ });
153
+
154
+ it('should calculate horizontal scroll blit (right)', () => {
155
+ const result = calculateBlit(
156
+ { x: 100, y: 0 }, // Current
157
+ { x: 0, y: 0 }, // Last
158
+ viewportSize,
159
+ pinnedWidths
160
+ );
161
+
162
+ expect(result.canBlit).toBe(true);
163
+ expect(result.deltaX).toBe(100);
164
+ expect(result.deltaY).toBe(0);
165
+
166
+ // Scrolling right: left strip needs redraw
167
+ expect(result.dirtyRegions).toHaveLength(1);
168
+ expect(result.dirtyRegions[0].x).toBe(100); // left pinned width
169
+ expect(result.dirtyRegions[0].width).toBe(100);
170
+ });
171
+
172
+ it('should calculate horizontal scroll blit (left)', () => {
173
+ const result = calculateBlit(
174
+ { x: 0, y: 0 }, // Current
175
+ { x: 100, y: 0 }, // Last
176
+ viewportSize,
177
+ pinnedWidths
178
+ );
179
+
180
+ expect(result.canBlit).toBe(true);
181
+ expect(result.deltaX).toBe(-100);
182
+
183
+ // Scrolling left: right strip needs redraw
184
+ expect(result.dirtyRegions).toHaveLength(1);
185
+ });
186
+
187
+ it('should return full redraw for diagonal scroll', () => {
188
+ const result = calculateBlit(
189
+ { x: 50, y: 50 },
190
+ { x: 0, y: 0 },
191
+ viewportSize,
192
+ pinnedWidths
193
+ );
194
+
195
+ expect(result.canBlit).toBe(false);
196
+ expect(result.dirtyRegions[0].width).toBe(800);
197
+ expect(result.dirtyRegions[0].height).toBe(600);
198
+ });
199
+
200
+ it('should account for pinned widths', () => {
201
+ const result = calculateBlit(
202
+ { x: 100, y: 0 },
203
+ { x: 0, y: 0 },
204
+ viewportSize,
205
+ pinnedWidths
206
+ );
207
+
208
+ // Center region is between pinned columns
209
+ expect(result.sourceRect.x).toBe(100); // left pinned width
210
+ // Center width is 800 - 100 - 50 = 650
211
+ expect(result.sourceRect.width).toBe(550); // 650 - 100 (delta)
212
+ });
213
+ });
214
+
215
+ describe('blitLastFrame', () => {
216
+ beforeEach(() => {
217
+ (mockContext.drawImage as ReturnType<typeof vi.fn>).mockClear();
218
+ });
219
+
220
+ it('should return blitted=false when blitting not possible', () => {
221
+ const lastCanvas = new MockCanvas() as unknown as HTMLCanvasElement;
222
+
223
+ const result = blitLastFrame(
224
+ mockContext,
225
+ lastCanvas,
226
+ { x: 0, y: 0 },
227
+ { x: 0, y: 0 },
228
+ { width: 800, height: 600 },
229
+ { left: 0, right: 0 }
230
+ );
231
+
232
+ expect(result.blitted).toBe(false);
233
+ });
234
+
235
+ it('should perform blit when possible', () => {
236
+ const lastCanvas = new MockCanvas() as unknown as HTMLCanvasElement;
237
+
238
+ const result = blitLastFrame(
239
+ mockContext,
240
+ lastCanvas,
241
+ { x: 0, y: 100 },
242
+ { x: 0, y: 0 },
243
+ { width: 800, height: 600 },
244
+ { left: 0, right: 0 }
245
+ );
246
+
247
+ expect(result.blitted).toBe(true);
248
+ expect(result.regionsToDraw.length).toBeGreaterThan(0);
249
+ expect(mockContext.drawImage).toHaveBeenCalled();
250
+ });
251
+ });
252
+
253
+ describe('createBufferPair', () => {
254
+ it('should create front and back buffers', () => {
255
+ const buffers = createBufferPair(800, 600);
256
+
257
+ expect(buffers.front).toBeDefined();
258
+ expect(buffers.back).toBeDefined();
259
+ expect(buffers.frontCtx).toBeDefined();
260
+ expect(buffers.backCtx).toBeDefined();
261
+ });
262
+
263
+ it('should set correct dimensions', () => {
264
+ const buffers = createBufferPair(800, 600);
265
+
266
+ expect(buffers.front.width).toBe(800);
267
+ expect(buffers.front.height).toBe(600);
268
+ expect(buffers.back.width).toBe(800);
269
+ expect(buffers.back.height).toBe(600);
270
+ });
271
+
272
+ it('should apply DPR scaling', () => {
273
+ const buffers = createBufferPair(800, 600, 2);
274
+
275
+ expect(buffers.front.width).toBe(1600);
276
+ expect(buffers.front.height).toBe(1200);
277
+ expect(buffers.front.style.width).toBe('800px');
278
+ expect(buffers.front.style.height).toBe('600px');
279
+ });
280
+ });
281
+
282
+ describe('swapBuffers', () => {
283
+ it('should swap front and back buffers', () => {
284
+ const buffers = createBufferPair(800, 600);
285
+ const originalFront = buffers.front;
286
+ const originalBack = buffers.back;
287
+
288
+ swapBuffers(buffers);
289
+
290
+ expect(buffers.front).toBe(originalBack);
291
+ expect(buffers.back).toBe(originalFront);
292
+ });
293
+
294
+ it('should swap contexts too', () => {
295
+ const buffers = createBufferPair(800, 600);
296
+ const originalFrontCtx = buffers.frontCtx;
297
+
298
+ swapBuffers(buffers);
299
+
300
+ expect(buffers.frontCtx).toBe(buffers.backCtx);
301
+ });
302
+ });
303
+
304
+ describe('displayBuffer', () => {
305
+ beforeEach(() => {
306
+ (mockContext.drawImage as ReturnType<typeof vi.fn>).mockClear();
307
+ });
308
+
309
+ it('should copy buffer to display context', () => {
310
+ const buffer = new MockCanvas() as unknown as HTMLCanvasElement;
311
+
312
+ displayBuffer(mockContext, buffer);
313
+
314
+ expect(mockContext.drawImage).toHaveBeenCalled();
315
+ });
316
+ });
317
+
318
+ describe('resizeBufferPair', () => {
319
+ it('should resize both buffers', () => {
320
+ const buffers = createBufferPair(800, 600);
321
+
322
+ resizeBufferPair(buffers, 1024, 768);
323
+
324
+ expect(buffers.front.width).toBe(1024);
325
+ expect(buffers.front.height).toBe(768);
326
+ expect(buffers.back.width).toBe(1024);
327
+ expect(buffers.back.height).toBe(768);
328
+ });
329
+
330
+ it('should apply DPR on resize', () => {
331
+ const buffers = createBufferPair(800, 600);
332
+
333
+ resizeBufferPair(buffers, 1024, 768, 2);
334
+
335
+ expect(buffers.front.width).toBe(2048);
336
+ expect(buffers.front.height).toBe(1536);
337
+ });
338
+ });
339
+
340
+ describe('BlitState', () => {
341
+ let state: BlitState;
342
+
343
+ beforeEach(() => {
344
+ state = new BlitState();
345
+ });
346
+
347
+ describe('updateScroll', () => {
348
+ it('should update and return last scroll position', () => {
349
+ const last = state.updateScroll(100, 200);
350
+
351
+ expect(last.x).toBe(0);
352
+ expect(last.y).toBe(0);
353
+
354
+ const next = state.updateScroll(150, 250);
355
+ expect(next.x).toBe(100);
356
+ expect(next.y).toBe(200);
357
+ });
358
+ });
359
+
360
+ describe('getScroll', () => {
361
+ it('should return current scroll position', () => {
362
+ state.updateScroll(100, 200);
363
+
364
+ const scroll = state.getScroll();
365
+ expect(scroll.x).toBe(100);
366
+ expect(scroll.y).toBe(200);
367
+ });
368
+ });
369
+
370
+ describe('lastCanvas', () => {
371
+ it('should store and retrieve last canvas', () => {
372
+ const canvas = new MockCanvas() as unknown as HTMLCanvasElement;
373
+ canvas.width = 800;
374
+ canvas.height = 600;
375
+
376
+ state.setLastCanvas(canvas);
377
+
378
+ const lastCanvas = state.getLastCanvas();
379
+ expect(lastCanvas).not.toBeNull();
380
+ expect(lastCanvas!.width).toBe(800);
381
+ expect(lastCanvas!.height).toBe(600);
382
+ });
383
+
384
+ it('should report hasLastFrame correctly', () => {
385
+ expect(state.hasLastFrame()).toBe(false);
386
+
387
+ state.setLastCanvas(new MockCanvas() as unknown as HTMLCanvasElement);
388
+ expect(state.hasLastFrame()).toBe(true);
389
+ });
390
+ });
391
+
392
+ describe('reset', () => {
393
+ it('should reset all state', () => {
394
+ state.updateScroll(100, 200);
395
+ state.setLastCanvas(new MockCanvas() as unknown as HTMLCanvasElement);
396
+
397
+ state.reset();
398
+
399
+ expect(state.getScroll()).toEqual({ x: 0, y: 0 });
400
+ expect(state.hasLastFrame()).toBe(false);
401
+ });
402
+ });
403
+ });
404
+
405
+ describe('Constants', () => {
406
+ it('MIN_BLIT_DELTA should be reasonable', () => {
407
+ expect(MIN_BLIT_DELTA).toBeGreaterThan(0);
408
+ expect(MIN_BLIT_DELTA).toBeLessThan(10);
409
+ });
410
+
411
+ it('MAX_BLIT_DELTA_RATIO should be reasonable', () => {
412
+ expect(MAX_BLIT_DELTA_RATIO).toBeGreaterThan(0.5);
413
+ expect(MAX_BLIT_DELTA_RATIO).toBeLessThan(1);
414
+ });
415
+ });
416
+
417
+ describe('Edge cases', () => {
418
+ it('should handle zero viewport size', () => {
419
+ const result = calculateBlit(
420
+ { x: 0, y: 0 },
421
+ { x: 0, y: 0 },
422
+ { width: 0, height: 0 },
423
+ { left: 0, right: 0 }
424
+ );
425
+
426
+ expect(result.canBlit).toBe(false);
427
+ });
428
+
429
+ it('should handle scroll delta equal to viewport', () => {
430
+ const result = calculateBlit(
431
+ { x: 0, y: 600 },
432
+ { x: 0, y: 0 },
433
+ { width: 800, height: 600 },
434
+ { left: 0, right: 0 }
435
+ );
436
+
437
+ // Should not blit when delta equals viewport
438
+ expect(result.canBlit).toBe(false);
439
+ });
440
+
441
+ it('should handle large pinned widths', () => {
442
+ const result = calculateBlit(
443
+ { x: 0, y: 100 },
444
+ { x: 0, y: 0 },
445
+ { width: 800, height: 600 },
446
+ { left: 400, right: 300 }
447
+ );
448
+
449
+ // Very small center area (100px)
450
+ expect(result.canBlit).toBe(true);
451
+ });
452
+ });
453
+ });