argent-grid 0.1.0 → 0.2.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 (108) 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 +2 -2
  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/screenshots.spec.ts +52 -0
  28. package/e2e/theming.spec.ts +35 -0
  29. package/e2e/visual.spec.ts +91 -0
  30. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  31. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  32. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  33. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  37. package/package.json +20 -6
  38. package/plan.md +50 -18
  39. package/playwright.config.ts +38 -0
  40. package/setup-vitest.ts +10 -13
  41. package/src/lib/argent-grid.module.ts +10 -12
  42. package/src/lib/components/argent-grid.component.css +327 -76
  43. package/src/lib/components/argent-grid.component.html +186 -64
  44. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  45. package/src/lib/components/argent-grid.component.ts +642 -189
  46. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  47. package/src/lib/components/set-filter/set-filter.component.ts +302 -0
  48. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  49. package/src/lib/directives/click-outside.directive.ts +19 -0
  50. package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
  51. package/src/lib/rendering/canvas-renderer.ts +418 -305
  52. package/src/lib/rendering/live-data-handler.ts +110 -0
  53. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  54. package/src/lib/rendering/render/blit.spec.ts +16 -27
  55. package/src/lib/rendering/render/blit.ts +48 -36
  56. package/src/lib/rendering/render/cells.spec.ts +132 -0
  57. package/src/lib/rendering/render/cells.ts +46 -24
  58. package/src/lib/rendering/render/column-utils.ts +73 -0
  59. package/src/lib/rendering/render/hit-test.ts +55 -0
  60. package/src/lib/rendering/render/index.ts +79 -76
  61. package/src/lib/rendering/render/lines.ts +43 -43
  62. package/src/lib/rendering/render/primitives.ts +161 -0
  63. package/src/lib/rendering/render/theme.spec.ts +8 -12
  64. package/src/lib/rendering/render/theme.ts +7 -10
  65. package/src/lib/rendering/render/types.ts +2 -2
  66. package/src/lib/rendering/render/walk.spec.ts +35 -38
  67. package/src/lib/rendering/render/walk.ts +60 -50
  68. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  69. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  70. package/src/lib/rendering/utils/index.ts +1 -1
  71. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  72. package/src/lib/services/grid.service.spec.ts +1165 -201
  73. package/src/lib/services/grid.service.ts +819 -187
  74. package/src/lib/themes/parts/color-schemes.ts +132 -0
  75. package/src/lib/themes/parts/icon-sets.ts +258 -0
  76. package/src/lib/themes/theme-builder.ts +347 -0
  77. package/src/lib/themes/theme-quartz.ts +72 -0
  78. package/src/lib/themes/types.ts +238 -0
  79. package/src/lib/types/ag-grid-types.ts +73 -14
  80. package/src/public-api.ts +39 -9
  81. package/src/stories/Advanced.stories.ts +188 -0
  82. package/src/stories/ArgentGrid.stories.ts +277 -0
  83. package/src/stories/Benchmark.stories.ts +74 -0
  84. package/src/stories/CellRenderers.stories.ts +221 -0
  85. package/src/stories/Filtering.stories.ts +252 -0
  86. package/src/stories/Grouping.stories.ts +217 -0
  87. package/src/stories/Theming.stories.ts +124 -0
  88. package/src/stories/benchmark-wrapper.component.ts +315 -0
  89. package/tsconfig.storybook.json +10 -0
  90. package/vitest.config.ts +9 -9
  91. package/demo-app/README.md +0 -70
  92. package/demo-app/angular.json +0 -78
  93. package/demo-app/e2e/benchmark.spec.ts +0 -53
  94. package/demo-app/e2e/demo-page.spec.ts +0 -77
  95. package/demo-app/e2e/grid-features.spec.ts +0 -269
  96. package/demo-app/package-lock.json +0 -14023
  97. package/demo-app/package.json +0 -36
  98. package/demo-app/playwright-test-menu.js +0 -19
  99. package/demo-app/playwright.config.ts +0 -23
  100. package/demo-app/src/app/app.component.ts +0 -10
  101. package/demo-app/src/app/app.config.ts +0 -13
  102. package/demo-app/src/app/app.routes.ts +0 -7
  103. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  104. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  105. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  106. package/demo-app/src/index.html +0 -19
  107. package/demo-app/src/main.ts +0 -6
  108. package/demo-app/tsconfig.json +0 -31
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Live Data Handler for Canvas Renderer
3
+ *
4
+ * Manages high-frequency data updates and buffering.
5
+ */
6
+
7
+ import { GridApi } from '../types/ag-grid-types';
8
+
9
+ export class LiveDataHandler<TData = any> {
10
+ private updateBuffer: TData[] = [];
11
+ private updateBufferTimer: number | null = null;
12
+ private batchInterval = 100;
13
+ private rowIndexById: Map<string, number> = new Map();
14
+ private dirtyRows: Set<number> = new Set();
15
+
16
+ constructor(private gridApi: GridApi<TData>) {}
17
+
18
+ setBatchInterval(intervalMs: number): void {
19
+ this.batchInterval = Math.max(16, intervalMs);
20
+ }
21
+
22
+ addRowData(data: TData, immediate = false, onFlush: () => void): void {
23
+ this.updateBuffer.push(data);
24
+
25
+ // Index row by ID if available
26
+ const dataWithId = data as any;
27
+ if (dataWithId.id) {
28
+ const index = this.gridApi.getRowData().length + this.updateBuffer.length - 1;
29
+ this.rowIndexById.set(dataWithId.id, index);
30
+ }
31
+
32
+ if (immediate) {
33
+ this.flushUpdateBuffer(onFlush);
34
+ } else if (!this.updateBufferTimer) {
35
+ this.updateBufferTimer = window.setTimeout(() => {
36
+ this.flushUpdateBuffer(onFlush);
37
+ }, this.batchInterval);
38
+ }
39
+ }
40
+
41
+ flushUpdateBuffer(onFlush: () => void): void {
42
+ if (this.updateBuffer.length === 0) return;
43
+
44
+ if (this.updateBufferTimer) {
45
+ clearTimeout(this.updateBufferTimer);
46
+ this.updateBufferTimer = null;
47
+ }
48
+
49
+ const currentCount = this.gridApi.getDisplayedRowCount();
50
+
51
+ // Apply transaction
52
+ this.gridApi.applyTransaction({ add: this.updateBuffer });
53
+
54
+ // Mark new rows as dirty
55
+ for (let i = 0; i < this.updateBuffer.length; i++) {
56
+ this.dirtyRows.add(currentCount + i);
57
+ }
58
+
59
+ this.updateBuffer = [];
60
+ onFlush();
61
+ }
62
+
63
+ markRowDirty(rowIndex: number): void {
64
+ this.dirtyRows.add(rowIndex);
65
+ }
66
+
67
+ getDirtyRows(): Set<number> {
68
+ return this.dirtyRows;
69
+ }
70
+
71
+ clearDirtyRows(): void {
72
+ this.dirtyRows.clear();
73
+ }
74
+
75
+ updateRowById(id: string, updates: Partial<TData>): boolean {
76
+ const index = this.rowIndexById.get(id);
77
+ const rowData = this.gridApi.getRowData();
78
+ if (index === undefined || index >= rowData.length) {
79
+ return false;
80
+ }
81
+
82
+ this.gridApi.applyTransaction({ update: [{ ...rowData[index], ...updates }] });
83
+ this.markRowDirty(index);
84
+ return true;
85
+ }
86
+
87
+ removeRowById(id: string): boolean {
88
+ const index = this.rowIndexById.get(id);
89
+ if (index === undefined) {
90
+ return false;
91
+ }
92
+
93
+ const rowData = this.gridApi.getRowData();
94
+ this.gridApi.applyTransaction({ remove: [rowData[index]] });
95
+ this.rowIndexById.delete(id);
96
+ this.rebuildRowIndex();
97
+ return true;
98
+ }
99
+
100
+ rebuildRowIndex(): void {
101
+ this.rowIndexById.clear();
102
+ const rowData = this.gridApi.getRowData();
103
+ rowData.forEach((row, index) => {
104
+ const rowWithId = row as any;
105
+ if (rowWithId.id) {
106
+ this.rowIndexById.set(rowWithId.id, index);
107
+ }
108
+ });
109
+ }
110
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Live Data Optimizations for CanvasRenderer
3
+ *
4
+ * Performance optimizations for high-frequency data updates (10+ entries/sec):
5
+ * - Update batching (90% fewer renders)
6
+ * - Dirty row tracking (90% less rendering work)
7
+ * - Row ID indexing (O(1) updates instead of O(n))
8
+ *
9
+ * @see CanvasRenderer - Main renderer class that uses these optimizations
10
+ */
11
+
12
+ /**
13
+ * Mixin for live data optimizations
14
+ *
15
+ * Usage:
16
+ * ```typescript
17
+ * class OptimizedRenderer extends LiveDataOptimizations(CanvasRenderer) {
18
+ * // Has all live data optimization methods
19
+ * }
20
+ * ```
21
+ */
22
+ export function LiveDataOptimizations<TBase extends new (...args: any[]) => any>(Base: TBase) {
23
+ return class extends Base {
24
+ // Update batching
25
+ updateBuffer: any[] = [];
26
+ updateBufferTimer: number | null = null;
27
+ batchInterval = 100; // ms
28
+
29
+ // Dirty row tracking
30
+ dirtyRows: Set<number> = new Set();
31
+
32
+ // Row index by ID
33
+ rowIndexById: Map<string, number> = new Map();
34
+
35
+ /**
36
+ * Set update batching interval
37
+ */
38
+ setBatchInterval(intervalMs: number): void {
39
+ this.batchInterval = Math.max(16, intervalMs);
40
+ }
41
+
42
+ /**
43
+ * Add row data with batching
44
+ */
45
+ addRowData(data: any, immediate = false): void {
46
+ this.updateBuffer.push(data);
47
+
48
+ // Index by ID
49
+ if (data.id) {
50
+ const index = (this as any).rowData.length + this.updateBuffer.length - 1;
51
+ this.rowIndexById.set(data.id, index);
52
+ }
53
+
54
+ if (immediate) {
55
+ this.flushUpdateBuffer();
56
+ } else if (!this.updateBufferTimer) {
57
+ this.updateBufferTimer = window.setTimeout(() => {
58
+ this.flushUpdateBuffer();
59
+ }, this.batchInterval);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Flush update buffer
65
+ */
66
+ flushUpdateBuffer(): void {
67
+ if (this.updateBuffer.length === 0) return;
68
+
69
+ if (this.updateBufferTimer) {
70
+ clearTimeout(this.updateBufferTimer);
71
+ this.updateBufferTimer = null;
72
+ }
73
+
74
+ const startIndex = (this as any).rowData.length;
75
+ (this as any).rowData.push(...this.updateBuffer);
76
+ this.updateBuffer = [];
77
+
78
+ // Mark new rows as dirty
79
+ for (let i = 0; i < (this as any).rowData.length - startIndex; i++) {
80
+ this.dirtyRows.add(startIndex + i);
81
+ }
82
+
83
+ (this as any).totalRowCount = (this as any).rowData.length;
84
+ (this as any).renderFrame();
85
+ }
86
+
87
+ /**
88
+ * Mark row as dirty
89
+ */
90
+ markRowDirty(rowIndex: number): void {
91
+ this.dirtyRows.add(rowIndex);
92
+ }
93
+
94
+ /**
95
+ * Update row by ID (O(1))
96
+ */
97
+ updateRowById(id: string, updates: any): boolean {
98
+ const index = this.rowIndexById.get(id);
99
+ if (index === undefined || index >= (this as any).rowData.length) {
100
+ return false;
101
+ }
102
+
103
+ Object.assign((this as any).rowData[index], updates);
104
+ this.markRowDirty(index);
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Remove row by ID
110
+ */
111
+ removeRowById(id: string): boolean {
112
+ const index = this.rowIndexById.get(id);
113
+ if (index === undefined) return false;
114
+
115
+ (this as any).rowData.splice(index, 1);
116
+ this.rowIndexById.delete(id);
117
+ this.rebuildRowIndex();
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Rebuild row index
123
+ */
124
+ rebuildRowIndex(): void {
125
+ this.rowIndexById.clear();
126
+ (this as any).rowData.forEach((row: any, index: number) => {
127
+ if (row.id) {
128
+ this.rowIndexById.set(row.id, index);
129
+ }
130
+ });
131
+ }
132
+ };
133
+ }
@@ -5,18 +5,17 @@
5
5
  */
6
6
 
7
7
  import {
8
- shouldBlit,
9
- calculateBlit,
8
+ BlitState,
10
9
  blitLastFrame,
10
+ calculateBlit,
11
11
  createBufferPair,
12
- swapBuffers,
13
12
  displayBuffer,
14
- resizeBufferPair,
15
- BlitState,
16
- MIN_BLIT_DELTA,
17
13
  MAX_BLIT_DELTA_RATIO,
14
+ MIN_BLIT_DELTA,
15
+ resizeBufferPair,
16
+ shouldBlit,
17
+ swapBuffers,
18
18
  } from './blit';
19
- import { Rectangle, BufferPair } from './types';
20
19
 
21
20
  // Mock canvas and context
22
21
  const mockContext = {
@@ -114,7 +113,7 @@ describe('Blitting Optimization', () => {
114
113
  it('should calculate vertical scroll blit (down)', () => {
115
114
  const result = calculateBlit(
116
115
  { x: 0, y: 100 }, // Current
117
- { x: 0, y: 0 }, // Last
116
+ { x: 0, y: 0 }, // Last
118
117
  viewportSize,
119
118
  pinnedWidths
120
119
  );
@@ -136,7 +135,7 @@ describe('Blitting Optimization', () => {
136
135
 
137
136
  it('should calculate vertical scroll blit (up)', () => {
138
137
  const result = calculateBlit(
139
- { x: 0, y: 0 }, // Current
138
+ { x: 0, y: 0 }, // Current
140
139
  { x: 0, y: 100 }, // Last
141
140
  viewportSize,
142
141
  pinnedWidths
@@ -154,7 +153,7 @@ describe('Blitting Optimization', () => {
154
153
  it('should calculate horizontal scroll blit (right)', () => {
155
154
  const result = calculateBlit(
156
155
  { x: 100, y: 0 }, // Current
157
- { x: 0, y: 0 }, // Last
156
+ { x: 0, y: 0 }, // Last
158
157
  viewportSize,
159
158
  pinnedWidths
160
159
  );
@@ -171,7 +170,7 @@ describe('Blitting Optimization', () => {
171
170
 
172
171
  it('should calculate horizontal scroll blit (left)', () => {
173
172
  const result = calculateBlit(
174
- { x: 0, y: 0 }, // Current
173
+ { x: 0, y: 0 }, // Current
175
174
  { x: 100, y: 0 }, // Last
176
175
  viewportSize,
177
176
  pinnedWidths
@@ -185,12 +184,7 @@ describe('Blitting Optimization', () => {
185
184
  });
186
185
 
187
186
  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
- );
187
+ const result = calculateBlit({ x: 50, y: 50 }, { x: 0, y: 0 }, viewportSize, pinnedWidths);
194
188
 
195
189
  expect(result.canBlit).toBe(false);
196
190
  expect(result.dirtyRegions[0].width).toBe(800);
@@ -198,12 +192,7 @@ describe('Blitting Optimization', () => {
198
192
  });
199
193
 
200
194
  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
- );
195
+ const result = calculateBlit({ x: 100, y: 0 }, { x: 0, y: 0 }, viewportSize, pinnedWidths);
207
196
 
208
197
  // Center region is between pinned columns
209
198
  expect(result.sourceRect.x).toBe(100); // left pinned width
@@ -293,7 +282,7 @@ describe('Blitting Optimization', () => {
293
282
 
294
283
  it('should swap contexts too', () => {
295
284
  const buffers = createBufferPair(800, 600);
296
- const originalFrontCtx = buffers.frontCtx;
285
+ const _originalFrontCtx = buffers.frontCtx;
297
286
 
298
287
  swapBuffers(buffers);
299
288
 
@@ -377,8 +366,8 @@ describe('Blitting Optimization', () => {
377
366
 
378
367
  const lastCanvas = state.getLastCanvas();
379
368
  expect(lastCanvas).not.toBeNull();
380
- expect(lastCanvas!.width).toBe(800);
381
- expect(lastCanvas!.height).toBe(600);
369
+ expect(lastCanvas?.width).toBe(800);
370
+ expect(lastCanvas?.height).toBe(600);
382
371
  });
383
372
 
384
373
  it('should report hasLastFrame correctly', () => {
@@ -450,4 +439,4 @@ describe('Blitting Optimization', () => {
450
439
  expect(result.canBlit).toBe(true);
451
440
  });
452
441
  });
453
- });
442
+ });
@@ -5,7 +5,7 @@
5
5
  * Based on Glide Data Grid's blitting architecture.
6
6
  */
7
7
 
8
- import { Rectangle, BlitResult, BufferPair } from './types';
8
+ import { BlitResult, BufferPair, Rectangle } from './types';
9
9
 
10
10
  // ============================================================================
11
11
  // BLIT THRESHOLDS
@@ -99,7 +99,7 @@ export function calculateBlit(
99
99
  };
100
100
  }
101
101
 
102
- const dirtyRegions: Rectangle[] = [];
102
+ const _dirtyRegions: Rectangle[] = [];
103
103
 
104
104
  // Vertical scroll (most common)
105
105
  if (Math.abs(deltaY) >= MIN_BLIT_DELTA && Math.abs(deltaX) < MIN_BLIT_DELTA) {
@@ -127,19 +127,27 @@ export function calculateBlit(
127
127
  },
128
128
  dirtyRegions: [
129
129
  // Top strip that needs redraw
130
- ...(deltaY > 0 ? [{
131
- x: leftPinnedWidth,
132
- y: 0,
133
- width: centerWidth,
134
- height: absDelta,
135
- }] : []),
130
+ ...(deltaY > 0
131
+ ? [
132
+ {
133
+ x: leftPinnedWidth,
134
+ y: 0,
135
+ width: centerWidth,
136
+ height: absDelta,
137
+ },
138
+ ]
139
+ : []),
136
140
  // Bottom strip that needs redraw
137
- ...(deltaY < 0 ? [{
138
- x: leftPinnedWidth,
139
- y: viewportSize.height - absDelta,
140
- width: centerWidth,
141
- height: absDelta,
142
- }] : []),
141
+ ...(deltaY < 0
142
+ ? [
143
+ {
144
+ x: leftPinnedWidth,
145
+ y: viewportSize.height - absDelta,
146
+ width: centerWidth,
147
+ height: absDelta,
148
+ },
149
+ ]
150
+ : []),
143
151
  ],
144
152
  deltaX,
145
153
  deltaY,
@@ -171,19 +179,27 @@ export function calculateBlit(
171
179
  },
172
180
  dirtyRegions: [
173
181
  // Left strip that needs redraw
174
- ...(deltaX > 0 ? [{
175
- x: leftPinnedWidth,
176
- y: 0,
177
- width: absDelta,
178
- height: viewportSize.height,
179
- }] : []),
182
+ ...(deltaX > 0
183
+ ? [
184
+ {
185
+ x: leftPinnedWidth,
186
+ y: 0,
187
+ width: absDelta,
188
+ height: viewportSize.height,
189
+ },
190
+ ]
191
+ : []),
180
192
  // Right strip that needs redraw
181
- ...(deltaX < 0 ? [{
182
- x: viewportSize.width - rightPinnedWidth - absDelta,
183
- y: 0,
184
- width: absDelta,
185
- height: viewportSize.height,
186
- }] : []),
193
+ ...(deltaX < 0
194
+ ? [
195
+ {
196
+ x: viewportSize.width - rightPinnedWidth - absDelta,
197
+ y: 0,
198
+ width: absDelta,
199
+ height: viewportSize.height,
200
+ },
201
+ ]
202
+ : []),
187
203
  ],
188
204
  deltaX,
189
205
  deltaY,
@@ -248,15 +264,11 @@ export function blitLastFrame(
248
264
  /**
249
265
  * Create a buffer pair for double buffering
250
266
  */
251
- export function createBufferPair(
252
- width: number,
253
- height: number,
254
- dpr: number = 1
255
- ): BufferPair {
267
+ export function createBufferPair(width: number, height: number, dpr: number = 1): BufferPair {
256
268
  const front = document.createElement('canvas');
257
269
  const back = document.createElement('canvas');
258
270
 
259
- [front, back].forEach(canvas => {
271
+ [front, back].forEach((canvas) => {
260
272
  canvas.width = Math.floor(width * dpr);
261
273
  canvas.height = Math.floor(height * dpr);
262
274
  canvas.style.width = `${width}px`;
@@ -307,7 +319,7 @@ export function resizeBufferPair(
307
319
  height: number,
308
320
  dpr: number = 1
309
321
  ): void {
310
- [buffers.front, buffers.back].forEach(canvas => {
322
+ [buffers.front, buffers.back].forEach((canvas) => {
311
323
  canvas.width = Math.floor(width * dpr);
312
324
  canvas.height = Math.floor(height * dpr);
313
325
  canvas.style.width = `${width}px`;
@@ -356,14 +368,14 @@ export class BlitState {
356
368
  if (!this.lastCanvas) {
357
369
  this.lastCanvas = document.createElement('canvas');
358
370
  }
359
-
371
+
360
372
  // Only resize if dimensions changed, as setting width/height clears the canvas
361
373
  // and is an expensive operation.
362
374
  if (this.lastCanvas.width !== canvas.width || this.lastCanvas.height !== canvas.height) {
363
375
  this.lastCanvas.width = canvas.width;
364
376
  this.lastCanvas.height = canvas.height;
365
377
  }
366
-
378
+
367
379
  const ctx = this.lastCanvas.getContext('2d')!;
368
380
  ctx.drawImage(canvas, 0, 0);
369
381
  }
@@ -390,4 +402,4 @@ export class BlitState {
390
402
  this.lastScrollY = 0;
391
403
  this.lastCanvas = null;
392
404
  }
393
- }
405
+ }
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { ColDef, GridApi, IRowNode } from '../../types/ag-grid-types';
3
+ import { getFormattedValue, getValueByPath, stripHtmlTags } from './cells';
4
+
5
+ describe('cells.ts', () => {
6
+ describe('stripHtmlTags', () => {
7
+ it('should strip HTML tags from string', () => {
8
+ expect(stripHtmlTags('<span>active</span>')).toBe('active');
9
+ expect(stripHtmlTags('<div class="test">content</div>')).toBe('content');
10
+ });
11
+
12
+ it('should handle complex HTML', () => {
13
+ const html =
14
+ '<span style="color: green; padding: 4px 8px; background: green20; border-radius: 4px;">active</span>';
15
+ expect(stripHtmlTags(html)).toBe('active');
16
+ });
17
+
18
+ it('should handle empty string', () => {
19
+ expect(stripHtmlTags('')).toBe('');
20
+ });
21
+
22
+ it('should handle null/undefined', () => {
23
+ expect(stripHtmlTags(null as any)).toBe('');
24
+ expect(stripHtmlTags(undefined as any)).toBe('');
25
+ });
26
+
27
+ it('should handle plain text (no HTML)', () => {
28
+ expect(stripHtmlTags('plain text')).toBe('plain text');
29
+ });
30
+
31
+ it('should handle multiple tags', () => {
32
+ expect(stripHtmlTags('<strong><em>bold italic</em></strong>')).toBe('bold italic');
33
+ });
34
+ });
35
+
36
+ describe('getValueByPath', () => {
37
+ it('should get value from simple path', () => {
38
+ const obj = { name: 'John', age: 30 };
39
+ expect(getValueByPath(obj, 'name')).toBe('John');
40
+ expect(getValueByPath(obj, 'age')).toBe(30);
41
+ });
42
+
43
+ it('should get value from nested path', () => {
44
+ const obj = { user: { name: 'John', address: { city: 'NYC' } } };
45
+ expect(getValueByPath(obj, 'user.name')).toBe('John');
46
+ expect(getValueByPath(obj, 'user.address.city')).toBe('NYC');
47
+ });
48
+
49
+ it('should return undefined for missing path', () => {
50
+ const obj = { name: 'John' };
51
+ expect(getValueByPath(obj, 'age')).toBe(undefined);
52
+ expect(getValueByPath(obj, 'address.city')).toBe(undefined);
53
+ });
54
+
55
+ it('should handle null/undefined objects', () => {
56
+ expect(getValueByPath(null, 'name')).toBe(undefined);
57
+ expect(getValueByPath(undefined, 'name')).toBe(undefined);
58
+ });
59
+ });
60
+
61
+ describe('getFormattedValue', () => {
62
+ const mockApi = {} as GridApi;
63
+ const mockRowNode = { data: { name: 'John', age: 30 } } as IRowNode;
64
+
65
+ it('should return empty string for null/undefined', () => {
66
+ expect(getFormattedValue(null, null, null as any, mockRowNode, mockApi)).toBe('');
67
+ expect(getFormattedValue(undefined, null, null as any, mockRowNode, mockApi)).toBe('');
68
+ });
69
+
70
+ it('should use valueFormatter if provided', () => {
71
+ const colDef = {
72
+ valueFormatter: vi.fn((params) => `$${params.value}`),
73
+ } as ColDef;
74
+
75
+ const result = getFormattedValue(100, colDef, { salary: 100 }, mockRowNode, mockApi);
76
+ expect(result).toBe('$100');
77
+ expect(colDef.valueFormatter).toHaveBeenCalled();
78
+ });
79
+
80
+ it('should use cellRenderer and strip HTML tags', () => {
81
+ const colDef = {
82
+ cellRenderer: vi.fn((params) => `<span style="color: green">${params.value}</span>`),
83
+ } as ColDef;
84
+
85
+ const result = getFormattedValue(
86
+ 'active',
87
+ colDef,
88
+ { status: 'active' },
89
+ mockRowNode,
90
+ mockApi
91
+ );
92
+ expect(result).toBe('active'); // HTML stripped
93
+ expect(colDef.cellRenderer).toHaveBeenCalled();
94
+ });
95
+
96
+ it('should handle cellRenderer returning plain text', () => {
97
+ const colDef = {
98
+ cellRenderer: vi.fn((params) => params.value.toUpperCase()),
99
+ } as ColDef;
100
+
101
+ const result = getFormattedValue('hello', colDef, { text: 'hello' }, mockRowNode, mockApi);
102
+ expect(result).toBe('HELLO');
103
+ });
104
+
105
+ it('should convert value to string if no formatter/renderer', () => {
106
+ const result = getFormattedValue(123, null, null as any, mockRowNode, mockApi);
107
+ expect(result).toBe('123');
108
+ });
109
+
110
+ it('should handle cellRenderer errors gracefully', () => {
111
+ const colDef = {
112
+ cellRenderer: vi.fn(() => {
113
+ throw new Error('Renderer error');
114
+ }),
115
+ } as ColDef;
116
+
117
+ const result = getFormattedValue('test', colDef, null as any, mockRowNode, mockApi);
118
+ expect(result).toBe('test'); // Falls back to value
119
+ });
120
+
121
+ it('should handle valueFormatter errors gracefully', () => {
122
+ const colDef = {
123
+ valueFormatter: vi.fn(() => {
124
+ throw new Error('Formatter error');
125
+ }),
126
+ } as ColDef;
127
+
128
+ const result = getFormattedValue('test', colDef, null as any, mockRowNode, mockApi);
129
+ expect(result).toBe('test'); // Falls back to value
130
+ });
131
+ });
132
+ });