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,513 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { GridApi } from '../types/ag-grid-types';
3
+ import { CanvasRenderer } from './canvas-renderer';
4
+
5
+ // Mock canvas context with ALL required methods
6
+ const mockCanvasContext = {
7
+ fillRect: vi.fn(),
8
+ clearRect: vi.fn(),
9
+ fillText: vi.fn(),
10
+ measureText: vi.fn(() => ({ width: 100 })),
11
+ stroke: vi.fn(),
12
+ beginPath: vi.fn(),
13
+ moveTo: vi.fn(),
14
+ lineTo: vi.fn(),
15
+ arc: vi.fn(),
16
+ fill: vi.fn(),
17
+ clip: vi.fn(),
18
+ save: vi.fn(),
19
+ restore: vi.fn(),
20
+ scale: vi.fn(),
21
+ setTransform: vi.fn(),
22
+ drawImage: vi.fn(),
23
+ createPattern: vi.fn(),
24
+ translate: vi.fn(),
25
+ rotate: vi.fn(),
26
+ transform: vi.fn(),
27
+ isPointInPath: vi.fn(),
28
+ strokeRect: vi.fn(),
29
+ strokeText: vi.fn(),
30
+ rect: vi.fn(),
31
+ lineCap: 'butt',
32
+ lineJoin: 'miter',
33
+ lineWidth: 1,
34
+ miterLimit: 10,
35
+ fillStyle: '#000000',
36
+ strokeStyle: '#000000',
37
+ font: '13px sans-serif',
38
+ textAlign: 'start',
39
+ textBaseline: 'alphabetic',
40
+ } as unknown as CanvasRenderingContext2D;
41
+
42
+ // Mock GridApi
43
+ const createMockGridApi = (): GridApi => {
44
+ return {
45
+ getGridId: vi.fn(() => 'test-grid'),
46
+ getColumnDefs: vi.fn(() => []),
47
+ setColumnDefs: vi.fn(),
48
+ getColumn: vi.fn(),
49
+ getAllColumns: vi.fn(() => []),
50
+ getDisplayedRowAtIndex: vi.fn(),
51
+ getDisplayedRowCount: vi.fn(() => 100),
52
+ getFirstRenderedRow: vi.fn(() => 0),
53
+ getLastRenderedRow: vi.fn(() => 100),
54
+ getFirstRenderedColumn: vi.fn(),
55
+ getLastRenderedColumn: vi.fn(),
56
+ forEachNode: vi.fn(),
57
+ forEachNodeAfterFilter: vi.fn(),
58
+ forEachNodeAfterFilterAndSort: vi.fn(),
59
+ getRowData: vi.fn(() => []),
60
+ setRowData: vi.fn(),
61
+ addItems: vi.fn(),
62
+ updateRowData: vi.fn(),
63
+ applyTransaction: vi.fn(),
64
+ selectIndex: vi.fn(),
65
+ selectNodes: vi.fn(),
66
+ deselectAll: vi.fn(),
67
+ getSelectedNodes: vi.fn(() => []),
68
+ getSelectedRows: vi.fn(() => []),
69
+ isRowSelected: vi.fn(() => false),
70
+ getFilterInstance: vi.fn(),
71
+ getFilterModel: vi.fn(() => ({})),
72
+ setFilterModel: vi.fn(),
73
+ onFilterChanged: vi.fn(),
74
+ getSortModel: vi.fn(() => []),
75
+ setSortModel: vi.fn(),
76
+ onSortChanged: vi.fn(),
77
+ sizeColumnsToFit: vi.fn(),
78
+ autoSizeColumns: vi.fn(),
79
+ setColumnWidth: vi.fn(),
80
+ getColumnState: vi.fn(() => []),
81
+ setColumnState: vi.fn(),
82
+ resetColumnState: vi.fn(),
83
+ exportDataAsCsv: vi.fn(),
84
+ exportDataAsExcel: vi.fn(),
85
+ copySelectedRowsToClipboard: vi.fn(),
86
+ copySelectedRangeToClipboard: vi.fn(),
87
+ copyRangeToClipboard: vi.fn(),
88
+ pasteFromClipboard: vi.fn(),
89
+ enableBrowserTooltips: vi.fn(),
90
+ showLoadingOverlay: vi.fn(),
91
+ hideOverlay: vi.fn(),
92
+ getFocusedCell: vi.fn(),
93
+ startEditingCell: vi.fn(),
94
+ stopEditingCell: vi.fn(),
95
+ getEditingCells: vi.fn(() => []),
96
+ refreshCells: vi.fn(),
97
+ refreshRows: vi.fn(),
98
+ redrawRows: vi.fn(),
99
+ refreshHeader: vi.fn(),
100
+ refreshFooter: vi.fn(),
101
+ refreshPivot: vi.fn(),
102
+ resetRowGroupColumns: vi.fn(),
103
+ addRowGroupColumn: vi.fn(),
104
+ removeRowGroupColumn: vi.fn(),
105
+ setRowGroupColumns: vi.fn(),
106
+ addPivotColumn: vi.fn(),
107
+ removePivotColumn: vi.fn(),
108
+ setPivotColumns: vi.fn(),
109
+ addValueColumn: vi.fn(),
110
+ removeValueColumn: vi.fn(),
111
+ setValueColumns: vi.fn(),
112
+ getRowGroupColumns: vi.fn(() => []),
113
+ getPivotColumns: vi.fn(() => []),
114
+ getValueColumns: vi.fn(() => []),
115
+ isPivotMode: vi.fn(() => false),
116
+ getPivotMode: vi.fn(() => false),
117
+ setPivotMode: vi.fn(),
118
+ getGroupDisplayType: vi.fn(() => 'singleColumn'),
119
+ getGroupRowRenderer: vi.fn(),
120
+ getRowHeight: vi.fn(() => 32),
121
+ getTotalRowHeight: vi.fn(() => 3200),
122
+ getScrollPosition: vi.fn(() => ({ top: 0, left: 0 })),
123
+ ensureIndexVisible: vi.fn(),
124
+ ensureColumnVisible: vi.fn(),
125
+ ensureColumnIndexVisible: vi.fn(),
126
+ flashCells: vi.fn(),
127
+ flashRows: vi.fn(),
128
+ getCellEditorFactory: vi.fn(),
129
+ registerCellRenderer: vi.fn(),
130
+ getRenderer: vi.fn(),
131
+ getEditor: vi.fn(),
132
+ getContextMenuItems: vi.fn(() => []),
133
+ getMainMenuItems: vi.fn(() => []),
134
+ showToolPanel: vi.fn(),
135
+ hideToolPanel: vi.fn(),
136
+ isToolPanelShowing: vi.fn(() => false),
137
+ setToolPanel: vi.fn(),
138
+ getToolPanel: vi.fn(),
139
+ destroy: vi.fn(),
140
+ addGlobalListener: vi.fn(),
141
+ removeGlobalListener: vi.fn(),
142
+ dispatchEvent: vi.fn(),
143
+ getEventPath: vi.fn(() => []),
144
+ getApi: vi.fn(),
145
+ getColumnApi: vi.fn(),
146
+ getLocaleText: vi.fn(),
147
+ getDocument: vi.fn(() => document),
148
+ getGridOptions: vi.fn(),
149
+ getRowPinned: vi.fn(),
150
+ getTopLevelNodes: vi.fn(() => []),
151
+ getRootNode: vi.fn(),
152
+ getRowNode: vi.fn(),
153
+ getModel: vi.fn(),
154
+ getRowModel: vi.fn(),
155
+ getPaginationPageSize: vi.fn(),
156
+ setPaginationPageSize: vi.fn(),
157
+ paginationGetPageSize: vi.fn(),
158
+ paginationSetPageSize: vi.fn(),
159
+ paginationGetRowCount: vi.fn(() => 100),
160
+ paginationGetCurrentPage: vi.fn(() => 0),
161
+ paginationGetTotalPages: vi.fn(() => 10),
162
+ paginationGoToFirstPage: vi.fn(),
163
+ paginationGoToLastPage: vi.fn(),
164
+ paginationGoToNextPage: vi.fn(),
165
+ paginationGoToPreviousPage: vi.fn(),
166
+ paginationGoToPage: vi.fn(),
167
+ isPaginationEnabled: vi.fn(() => false),
168
+ getCellRanges: vi.fn(() => null),
169
+ getRowAtY: vi.fn((y: number) => Math.floor(y / 32)),
170
+ getRowY: vi.fn((rowIndex: number) => rowIndex * 32),
171
+ } as any;
172
+ };
173
+
174
+ describe('CanvasRenderer', () => {
175
+ let renderer: CanvasRenderer;
176
+ let mockApi: GridApi;
177
+ let mockContainer: HTMLDivElement;
178
+ let mockCanvas: HTMLCanvasElement;
179
+
180
+ beforeAll(() => {
181
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(
182
+ () => mockCanvasContext as any
183
+ );
184
+ vi.spyOn(console, 'error').mockImplementation(() => {});
185
+ });
186
+
187
+ afterAll(() => {
188
+ vi.restoreAllMocks();
189
+ });
190
+
191
+ beforeEach(() => {
192
+ // Create a real canvas element for proper DOM behavior
193
+ mockCanvas = document.createElement('canvas');
194
+ mockCanvas.width = 800;
195
+ mockCanvas.height = 600;
196
+
197
+ // Mock scrollHeight and clientHeight using Object.defineProperty
198
+ Object.defineProperty(mockCanvas, 'scrollHeight', {
199
+ value: 3200,
200
+ writable: true,
201
+ configurable: true,
202
+ });
203
+ Object.defineProperty(mockCanvas, 'clientHeight', {
204
+ value: 600,
205
+ writable: true,
206
+ configurable: true,
207
+ });
208
+
209
+ mockApi = createMockGridApi();
210
+ mockContainer = document.createElement('div');
211
+ mockContainer.style.height = '600px';
212
+ mockContainer.style.overflow = 'auto';
213
+ mockContainer.appendChild(mockCanvas);
214
+
215
+ vi.spyOn(mockCanvas, 'parentElement', 'get').mockReturnValue(mockContainer);
216
+ vi.spyOn(mockContainer, 'scrollHeight', 'get').mockReturnValue(3200);
217
+ vi.spyOn(mockContainer, 'clientHeight', 'get').mockReturnValue(600);
218
+
219
+ renderer = new CanvasRenderer(mockCanvas, mockApi, 32);
220
+ });
221
+
222
+ it('should create', () => {
223
+ expect(renderer).toBeTruthy();
224
+ });
225
+
226
+ it('should initialize with correct row height', () => {
227
+ // Renderer should be initialized
228
+ expect(mockCanvas).toBeTruthy();
229
+ });
230
+
231
+ it('should resize canvas', () => {
232
+ const resizeSpy = vi.spyOn(renderer as any, 'resize');
233
+ window.dispatchEvent(new Event('resize'));
234
+ vi.advanceTimersByTime(200);
235
+ expect(resizeSpy).toHaveBeenCalled();
236
+ });
237
+
238
+ it('should render frame', () => {
239
+ expect(() => renderer.renderFrame()).not.toThrow();
240
+ });
241
+
242
+ it('should scrollToBottom', () => {
243
+ const container = mockCanvas.parentElement!;
244
+ const scrollTopSpy = vi.spyOn(container, 'scrollTop', 'set');
245
+ renderer.scrollToBottom();
246
+ expect(scrollTopSpy).toHaveBeenCalled();
247
+ });
248
+
249
+ it('should handle context menu events', () => {
250
+ const event = new MouseEvent('contextmenu', { bubbles: true });
251
+ mockCanvas.dispatchEvent(event);
252
+ expect(event.defaultPrevented).toBe(false);
253
+ });
254
+
255
+ it('should get column at position', () => {
256
+ const column = renderer.getColumnAtPosition(100);
257
+ expect(typeof column).toBe('number');
258
+ });
259
+
260
+ it('should get row at position', () => {
261
+ const row = renderer.getRowAtPosition(100);
262
+ expect(typeof row).toBe('number');
263
+ });
264
+
265
+ it('should handle viewport changes', () => {
266
+ const container = mockCanvas.parentElement!;
267
+ Object.defineProperty(container, 'clientHeight', {
268
+ value: 800,
269
+ writable: true,
270
+ configurable: true,
271
+ });
272
+ window.dispatchEvent(new Event('resize'));
273
+ expect(container).toBeTruthy();
274
+ });
275
+
276
+ it('should handle render with damage tracking', () => {
277
+ expect(() => renderer.render()).not.toThrow();
278
+ });
279
+
280
+ it('should handle multiple invalidations', () => {
281
+ renderer.render();
282
+ renderer.render();
283
+ renderer.render();
284
+ expect(renderer).toBeTruthy();
285
+ });
286
+
287
+ it('should handle event listener cleanup', () => {
288
+ const removeSpy = vi.spyOn(mockCanvas, 'removeEventListener');
289
+ renderer.destroy();
290
+ expect(removeSpy).toHaveBeenCalled();
291
+ removeSpy.mockRestore();
292
+ });
293
+
294
+ it('should render checkbox for dedicated selection column', () => {
295
+ const mockColumn = {
296
+ colId: 'ag-Grid-SelectionColumn',
297
+ width: 50,
298
+ visible: true,
299
+ pinned: 'left',
300
+ checkboxSelection: true,
301
+ } as any;
302
+
303
+ const mockRowNode = {
304
+ data: { id: 1, name: 'Test' },
305
+ selected: true,
306
+ rowIndex: 0,
307
+ displayedRowIndex: 0,
308
+ group: false,
309
+ master: false,
310
+ level: 0,
311
+ expanded: false,
312
+ detail: false,
313
+ } as any;
314
+
315
+ vi.spyOn(mockApi, 'getAllColumns').mockReturnValue([mockColumn]);
316
+ vi.spyOn(mockApi, 'getColumnDefs').mockReturnValue([mockColumn]);
317
+ vi.spyOn(mockApi, 'getDisplayedRowAtIndex').mockReturnValue(mockRowNode);
318
+ vi.spyOn(mockApi, 'getDisplayedRowCount').mockReturnValue(1);
319
+
320
+ // Mark dirty so doRender doesn't skip, then force a synchronous render
321
+ renderer.invalidateAll();
322
+ renderer.renderFrame();
323
+
324
+ // Verify strokeRect was called (for checkbox border)
325
+ expect(mockCanvasContext.strokeRect).toHaveBeenCalled();
326
+ });
327
+
328
+ // ============================================================================
329
+ // REGRESSION TESTS — Canvas Renderer architectural invariants
330
+ // These tests guard against the performance / feedback-loop bugs fixed in
331
+ // Phase VII. Do NOT remove them.
332
+ // ============================================================================
333
+
334
+ describe('Damage gate — scheduleRender() must not queue rAF when nothing is dirty', () => {
335
+ it('does not call requestAnimationFrame when damage tracker has no damage', () => {
336
+ const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation(() => 0);
337
+
338
+ // Brand-new renderer has no pending damage after construction — call
339
+ // scheduleRender indirectly through the private accessor by calling
340
+ // render() and then draining the dirty state first.
341
+ // Start clean: call renderFrame() to drain any initial dirty state.
342
+ renderer.renderFrame();
343
+
344
+ // Reset the spy AFTER the initial frame so we only measure new calls.
345
+ rafSpy.mockClear();
346
+
347
+ // scheduleRender() with no damage should be a no-op.
348
+ (renderer as any).scheduleRender();
349
+ expect(rafSpy).not.toHaveBeenCalled();
350
+
351
+ rafSpy.mockRestore();
352
+ });
353
+
354
+ it('calls requestAnimationFrame when damage is present', () => {
355
+ let rafCalled = false;
356
+ const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation((cb) => {
357
+ rafCalled = true;
358
+ return 0;
359
+ });
360
+
361
+ // Mark dirty then schedule.
362
+ renderer.render(); // internally calls markAllDirty() + scheduleRender()
363
+ expect(rafCalled).toBe(true);
364
+
365
+ rafSpy.mockRestore();
366
+ });
367
+ });
368
+
369
+ describe('nextRenderPending coalescing — concurrent scheduleRender calls must not drop renders', () => {
370
+ it('sets nextRenderPending when a frame is already in-flight', () => {
371
+ let _capturedCb: FrameRequestCallback | null = null;
372
+ const rafSpy = vi
373
+ .spyOn(globalThis, 'requestAnimationFrame')
374
+ .mockImplementation((cb: FrameRequestCallback) => {
375
+ _capturedCb = cb;
376
+ return 1;
377
+ });
378
+
379
+ // First call — starts the rAF.
380
+ renderer.render();
381
+ expect((renderer as any).animationFrameId).not.toBeNull();
382
+ expect((renderer as any).nextRenderPending).toBe(false);
383
+
384
+ // Second call while frame is still in-flight.
385
+ renderer.render();
386
+ expect((renderer as any).nextRenderPending).toBe(true);
387
+
388
+ rafSpy.mockRestore();
389
+ });
390
+
391
+ it('schedules a follow-up render after the first frame completes if damage arrives mid-render', () => {
392
+ const frames: FrameRequestCallback[] = [];
393
+ const rafSpy = vi
394
+ .spyOn(globalThis, 'requestAnimationFrame')
395
+ .mockImplementation((cb: FrameRequestCallback) => {
396
+ frames.push(cb);
397
+ return frames.length;
398
+ });
399
+
400
+ // Patch doRender to mark dirty again mid-render (simulates data arriving during a frame).
401
+ const originalDoRender = (renderer as any).doRender.bind(renderer);
402
+ vi.spyOn(renderer as any, 'doRender').mockImplementationOnce(() => {
403
+ originalDoRender();
404
+ // Simulate new data arriving while the frame is being painted.
405
+ renderer.render(); // marks dirty + sets nextRenderPending (animationFrameId still set)
406
+ });
407
+
408
+ renderer.render(); // queues frame[0]
409
+ expect(frames.length).toBe(1);
410
+
411
+ // Fire the first frame — mid-render, render() is called again marking dirty.
412
+ frames[0](0);
413
+ // A second rAF should have been scheduled for the newly dirty state.
414
+ expect(frames.length).toBe(2);
415
+
416
+ rafSpy.mockRestore();
417
+ });
418
+ });
419
+
420
+ describe('updateCanvasSize — must NEVER touch canvas.style.width or canvas.style.height', () => {
421
+ it('does not set canvas.style.width after setViewportDimensions', () => {
422
+ mockCanvas.style.width = ''; // clear before test
423
+ renderer.setViewportDimensions(640, 480);
424
+ expect(mockCanvas.style.width).toBe('');
425
+ });
426
+
427
+ it('does not set canvas.style.height after setViewportDimensions', () => {
428
+ mockCanvas.style.height = ''; // clear before test
429
+ renderer.setViewportDimensions(640, 480);
430
+ expect(mockCanvas.style.height).toBe('');
431
+ });
432
+
433
+ it('does set canvas.width (pixel buffer) after setViewportDimensions', () => {
434
+ renderer.setViewportDimensions(640, 480);
435
+ const dpr = window.devicePixelRatio || 1;
436
+ expect(mockCanvas.width).toBe(640 * dpr);
437
+ });
438
+
439
+ it('does set canvas.height (pixel buffer) after setViewportDimensions', () => {
440
+ renderer.setViewportDimensions(640, 480);
441
+ const dpr = window.devicePixelRatio || 1;
442
+ expect(mockCanvas.height).toBe(480 * dpr);
443
+ });
444
+ });
445
+
446
+ describe('blit infrastructure — removed dead code', () => {
447
+ it('does not have a blitState field (BlitState removed as dead code)', () => {
448
+ expect((renderer as any).blitState).toBeUndefined();
449
+ });
450
+
451
+ it('does not have a columnPositions field (dead map removed)', () => {
452
+ expect((renderer as any).columnPositions).toBeUndefined();
453
+ });
454
+
455
+ it('does not have a renderPending field (redundant with animationFrameId)', () => {
456
+ expect('renderPending' in renderer).toBe(false);
457
+ });
458
+ });
459
+
460
+ describe('renderThrottleMs — setRenderThrottle must not exist (removed in Phase VII)', () => {
461
+ it('does not expose a setRenderThrottle method', () => {
462
+ expect(typeof (renderer as any).setRenderThrottle).toBe('undefined');
463
+ });
464
+
465
+ it('does not have a renderThrottleMs property', () => {
466
+ expect('renderThrottleMs' in renderer).toBe(false);
467
+ });
468
+ });
469
+
470
+ it('should toggle row selection when clicking dedicated selection column', () => {
471
+ const mockColumn = {
472
+ colId: 'ag-Grid-SelectionColumn',
473
+ width: 50,
474
+ visible: true,
475
+ pinned: 'left',
476
+ checkboxSelection: true,
477
+ } as any;
478
+
479
+ const mockRowNode = {
480
+ data: { id: 1, name: 'Test' },
481
+ selected: false,
482
+ rowIndex: 0,
483
+ displayedRowIndex: 0,
484
+ setSelected: vi.fn(function (this: any, val) {
485
+ this.selected = val;
486
+ }),
487
+ group: false,
488
+ master: false,
489
+ level: 0,
490
+ expanded: false,
491
+ detail: false,
492
+ } as any;
493
+
494
+ vi.spyOn(mockApi, 'getAllColumns').mockReturnValue([mockColumn]);
495
+ vi.spyOn(mockApi, 'getColumnDefs').mockReturnValue([mockColumn]);
496
+ vi.spyOn(mockApi, 'getDisplayedRowAtIndex').mockReturnValue(mockRowNode);
497
+ vi.spyOn(mockApi, 'getDisplayedRowCount').mockReturnValue(1);
498
+
499
+ // Simulate click on checkbox area
500
+ const rect = mockCanvas.getBoundingClientRect();
501
+ const clickEvent = new MouseEvent('click', {
502
+ clientX: rect.left + 25, // Center of checkbox column
503
+ clientY: rect.top + 16, // Center of first row
504
+ bubbles: true,
505
+ });
506
+
507
+ mockCanvas.dispatchEvent(clickEvent);
508
+
509
+ // setSelected should have been called
510
+ expect(mockRowNode.setSelected).toHaveBeenCalledWith(true);
511
+ expect(mockRowNode.selected).toBe(true);
512
+ });
513
+ });