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.
- package/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- 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
|
+
});
|