@object-ui/plugin-grid 3.1.5 → 3.3.1

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 (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +649 -623
  5. package/dist/index.umd.cjs +8 -8
  6. package/package.json +45 -13
  7. package/.turbo/turbo-build.log +0 -32
  8. package/src/FormulaBar.tsx +0 -151
  9. package/src/GroupRow.tsx +0 -69
  10. package/src/ImportWizard.tsx +0 -412
  11. package/src/InlineEditing.tsx +0 -235
  12. package/src/ListColumnExtensions.test.tsx +0 -373
  13. package/src/ListColumnSchema.test.ts +0 -88
  14. package/src/ObjectGrid.EdgeCases.stories.tsx +0 -147
  15. package/src/ObjectGrid.msw.test.tsx +0 -130
  16. package/src/ObjectGrid.stories.tsx +0 -139
  17. package/src/ObjectGrid.tsx +0 -1596
  18. package/src/SplitPaneGrid.tsx +0 -120
  19. package/src/VirtualGrid.tsx +0 -183
  20. package/src/__tests__/GroupRow.test.tsx +0 -206
  21. package/src/__tests__/ImportPreview.test.tsx +0 -171
  22. package/src/__tests__/InlineEditing.test.tsx +0 -360
  23. package/src/__tests__/VirtualGrid.test.tsx +0 -438
  24. package/src/__tests__/accessibility.test.tsx +0 -254
  25. package/src/__tests__/accessorKey-inference.test.tsx +0 -132
  26. package/src/__tests__/airtable-style.test.tsx +0 -508
  27. package/src/__tests__/column-features.test.tsx +0 -490
  28. package/src/__tests__/grid-export.test.tsx +0 -121
  29. package/src/__tests__/mobile-card-view.test.tsx +0 -355
  30. package/src/__tests__/objectdef-enrichment.test.tsx +0 -566
  31. package/src/__tests__/performance-benchmark.test.tsx +0 -182
  32. package/src/__tests__/phase11-features.test.tsx +0 -418
  33. package/src/__tests__/row-bulk-actions.test.tsx +0 -413
  34. package/src/__tests__/row-height.test.tsx +0 -160
  35. package/src/__tests__/useGroupedData.test.ts +0 -165
  36. package/src/__tests__/view-states.test.tsx +0 -203
  37. package/src/components/BulkActionBar.tsx +0 -66
  38. package/src/components/RowActionMenu.tsx +0 -91
  39. package/src/index.test.tsx +0 -29
  40. package/src/index.tsx +0 -99
  41. package/src/useCellClipboard.ts +0 -136
  42. package/src/useColumnSummary.ts +0 -128
  43. package/src/useGradientColor.ts +0 -103
  44. package/src/useGroupReorder.ts +0 -123
  45. package/src/useGroupedData.ts +0 -187
  46. package/src/useRowColor.ts +0 -74
  47. package/tsconfig.json +0 -9
  48. package/vite.config.ts +0 -57
  49. package/vitest.config.ts +0 -13
  50. package/vitest.setup.ts +0 -1
  51. /package/dist/{plugin-grid → packages/plugin-grid}/src/FormulaBar.d.ts +0 -0
  52. /package/dist/{plugin-grid → packages/plugin-grid}/src/GroupRow.d.ts +0 -0
  53. /package/dist/{plugin-grid → packages/plugin-grid}/src/ImportWizard.d.ts +0 -0
  54. /package/dist/{plugin-grid → packages/plugin-grid}/src/InlineEditing.d.ts +0 -0
  55. /package/dist/{plugin-grid → packages/plugin-grid}/src/ObjectGrid.d.ts +0 -0
  56. /package/dist/{plugin-grid → packages/plugin-grid}/src/SplitPaneGrid.d.ts +0 -0
  57. /package/dist/{plugin-grid → packages/plugin-grid}/src/VirtualGrid.d.ts +0 -0
  58. /package/dist/{plugin-grid → packages/plugin-grid}/src/components/BulkActionBar.d.ts +0 -0
  59. /package/dist/{plugin-grid → packages/plugin-grid}/src/components/RowActionMenu.d.ts +0 -0
  60. /package/dist/{plugin-grid → packages/plugin-grid}/src/index.d.ts +0 -0
  61. /package/dist/{plugin-grid → packages/plugin-grid}/src/useCellClipboard.d.ts +0 -0
  62. /package/dist/{plugin-grid → packages/plugin-grid}/src/useColumnSummary.d.ts +0 -0
  63. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGradientColor.d.ts +0 -0
  64. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGroupReorder.d.ts +0 -0
  65. /package/dist/{plugin-grid → packages/plugin-grid}/src/useGroupedData.d.ts +0 -0
  66. /package/dist/{plugin-grid → packages/plugin-grid}/src/useRowColor.d.ts +0 -0
@@ -1,182 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- *
8
- * Performance benchmark tests for VirtualGrid.
9
- * Part of P2.4 Performance at Scale roadmap.
10
- */
11
-
12
- import { describe, it, expect, vi, beforeEach } from 'vitest';
13
- import { render, cleanup } from '@testing-library/react';
14
- import '@testing-library/jest-dom';
15
- import React from 'react';
16
- import type { VirtualGridColumn, VirtualGridProps } from '../VirtualGrid';
17
-
18
- // --- Mock @tanstack/react-virtual ---
19
- // Cap the visible window to simulate virtualisation (only render a subset)
20
- const VISIBLE_WINDOW = 50;
21
-
22
- vi.mock('@tanstack/react-virtual', () => ({
23
- useVirtualizer: (opts: any) => {
24
- const count: number = opts.count;
25
- const size: number = opts.estimateSize();
26
- const visible = Math.min(count, VISIBLE_WINDOW);
27
- const items = [];
28
- for (let i = 0; i < visible; i++) {
29
- items.push({ index: i, key: String(i), start: i * size, size });
30
- }
31
- return {
32
- getVirtualItems: () => items,
33
- getTotalSize: () => count * size,
34
- };
35
- },
36
- }));
37
-
38
- // --- Data generators ---
39
- function generateRows(count: number) {
40
- const rows = [];
41
- for (let i = 0; i < count; i++) {
42
- rows.push({
43
- id: i,
44
- name: `User ${i}`,
45
- email: `user${i}@example.com`,
46
- department: `Dept ${i % 10}`,
47
- salary: 50000 + (i * 100),
48
- });
49
- }
50
- return rows;
51
- }
52
-
53
- const benchmarkColumns: VirtualGridColumn[] = [
54
- { header: 'ID', accessorKey: 'id' },
55
- { header: 'Name', accessorKey: 'name' },
56
- { header: 'Email', accessorKey: 'email' },
57
- { header: 'Department', accessorKey: 'department' },
58
- { header: 'Salary', accessorKey: 'salary', align: 'right' },
59
- ];
60
-
61
- type VirtualGridComponent = React.FC<VirtualGridProps>;
62
-
63
- let VirtualGrid: VirtualGridComponent;
64
-
65
- beforeEach(async () => {
66
- cleanup();
67
- vi.resetModules();
68
- const mod = await import('../VirtualGrid');
69
- VirtualGrid = mod.VirtualGrid;
70
- });
71
-
72
- function renderGrid(overrides: Partial<VirtualGridProps> = {}) {
73
- const props: VirtualGridProps = {
74
- data: [],
75
- columns: benchmarkColumns,
76
- ...overrides,
77
- };
78
- return render(<VirtualGrid {...props} />);
79
- }
80
-
81
- // =========================================================================
82
- // Performance Benchmarks
83
- // =========================================================================
84
-
85
- describe('VirtualGrid: performance benchmarks', () => {
86
- it('renders 1,000 rows under 500ms', () => {
87
- const data = generateRows(1_000);
88
-
89
- const start = performance.now();
90
- const { container } = renderGrid({ data });
91
- const elapsed = performance.now() - start;
92
-
93
- expect(container.querySelector('.overflow-auto')).toBeInTheDocument();
94
- expect(elapsed).toBeLessThan(500);
95
- });
96
-
97
- it('renders 10,000 rows under 2,000ms', () => {
98
- const data = generateRows(10_000);
99
-
100
- const start = performance.now();
101
- const { container } = renderGrid({ data });
102
- const elapsed = performance.now() - start;
103
-
104
- expect(container.querySelector('.overflow-auto')).toBeInTheDocument();
105
- expect(elapsed).toBeLessThan(2_000);
106
- });
107
-
108
- it('renders 50,000 rows without crashing', () => {
109
- const data = generateRows(50_000);
110
-
111
- const start = performance.now();
112
- const { container } = renderGrid({ data });
113
- const elapsed = performance.now() - start;
114
-
115
- expect(container.querySelector('.overflow-auto')).toBeInTheDocument();
116
- // Virtual scrolling should keep render time manageable even at 50K
117
- expect(elapsed).toBeLessThan(10_000);
118
- });
119
-
120
- it('data generation for 10,000 rows is fast (< 200ms)', () => {
121
- const start = performance.now();
122
- const data = generateRows(10_000);
123
- const elapsed = performance.now() - start;
124
-
125
- expect(data).toHaveLength(10_000);
126
- expect(elapsed).toBeLessThan(200);
127
- });
128
- });
129
-
130
- // =========================================================================
131
- // Scaling characteristics
132
- // =========================================================================
133
-
134
- describe('VirtualGrid: scaling characteristics', () => {
135
- it('shows correct row count in footer for large dataset', () => {
136
- const data = generateRows(1_000);
137
- renderGrid({ data });
138
-
139
- // Footer displays total count regardless of virtualised window
140
- expect(document.body.textContent).toContain('1000');
141
- });
142
-
143
- it('renders with many columns (20+) without degradation', () => {
144
- const columns: VirtualGridColumn[] = Array.from({ length: 20 }, (_, i) => ({
145
- header: `Col ${i}`,
146
- accessorKey: `field${i}`,
147
- }));
148
- const data = Array.from({ length: 500 }, (_, row) =>
149
- Object.fromEntries(columns.map((c) => [c.accessorKey, `R${row}-${c.accessorKey}`])),
150
- );
151
-
152
- const start = performance.now();
153
- const { container } = renderGrid({ columns, data });
154
- const elapsed = performance.now() - start;
155
-
156
- expect(container.querySelector('.overflow-auto')).toBeInTheDocument();
157
- expect(elapsed).toBeLessThan(2_000);
158
- });
159
-
160
- it('custom cell renderers do not drastically increase render time for 1,000 rows', () => {
161
- const columnsWithRenderers: VirtualGridColumn[] = [
162
- {
163
- header: 'Name',
164
- accessorKey: 'name',
165
- cell: (value: string) => <strong>{value}</strong>,
166
- },
167
- {
168
- header: 'Email',
169
- accessorKey: 'email',
170
- cell: (value: string) => <a href={`mailto:${value}`}>{value}</a>,
171
- },
172
- { header: 'Department', accessorKey: 'department' },
173
- ];
174
- const data = generateRows(1_000);
175
-
176
- const start = performance.now();
177
- renderGrid({ columns: columnsWithRenderers, data });
178
- const elapsed = performance.now() - start;
179
-
180
- expect(elapsed).toBeLessThan(1_000);
181
- });
182
- });
@@ -1,418 +0,0 @@
1
- /**
2
- * Phase 11 – Grid & Table Excellence L2/L3 feature tests.
3
- *
4
- * Covers: useCellClipboard, useGradientColor, useGroupReorder,
5
- * FormulaBar, SplitPaneGrid, useGroupedData (aggregations).
6
- */
7
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
- import { render, screen, fireEvent, act } from '@testing-library/react';
9
- import { renderHook } from '@testing-library/react';
10
- import '@testing-library/jest-dom';
11
- import React from 'react';
12
-
13
- import { useCellClipboard } from '../useCellClipboard';
14
- import { useGradientColor } from '../useGradientColor';
15
- import { useGroupReorder } from '../useGroupReorder';
16
- import { FormulaBar } from '../FormulaBar';
17
- import { SplitPaneGrid } from '../SplitPaneGrid';
18
- import { useGroupedData } from '../useGroupedData';
19
- import type { AggregationConfig } from '../useGroupedData';
20
-
21
- // ---------------------------------------------------------------------------
22
- // Clipboard mock
23
- // ---------------------------------------------------------------------------
24
- let clipboardText = '';
25
- const clipboardMock = {
26
- writeText: vi.fn((text: string) => {
27
- clipboardText = text;
28
- return Promise.resolve();
29
- }),
30
- readText: vi.fn(() => Promise.resolve(clipboardText)),
31
- };
32
- Object.defineProperty(navigator, 'clipboard', {
33
- value: clipboardMock,
34
- writable: true,
35
- configurable: true,
36
- });
37
- beforeEach(() => {
38
- clipboardText = '';
39
- clipboardMock.writeText.mockClear();
40
- clipboardMock.readText.mockClear();
41
- });
42
-
43
- // =========================================================================
44
- // useCellClipboard
45
- // =========================================================================
46
- describe('useCellClipboard', () => {
47
- const data = [
48
- { name: 'Alice', age: '30', city: 'NYC' },
49
- { name: 'Bob', age: '25', city: 'LA' },
50
- { name: 'Carol', age: '35', city: 'SF' },
51
- ];
52
- const columns = ['name', 'age', 'city'];
53
-
54
- it('returns initial empty state', () => {
55
- const { result } = renderHook(() =>
56
- useCellClipboard({ data, columns }),
57
- );
58
- expect(result.current.selectedRange).toBeNull();
59
- });
60
-
61
- it('setSelectedRange updates range', () => {
62
- const { result } = renderHook(() =>
63
- useCellClipboard({ data, columns }),
64
- );
65
- act(() => {
66
- result.current.setSelectedRange({ startRow: 0, startCol: 0, endRow: 0, endCol: 0 });
67
- });
68
- expect(result.current.selectedRange).toEqual({
69
- startRow: 0, startCol: 0, endRow: 0, endCol: 0,
70
- });
71
- });
72
-
73
- it('clearSelection resets to null', () => {
74
- const { result } = renderHook(() =>
75
- useCellClipboard({ data, columns }),
76
- );
77
- act(() => {
78
- result.current.setSelectedRange({ startRow: 0, startCol: 0, endRow: 1, endCol: 1 });
79
- });
80
- act(() => {
81
- result.current.setSelectedRange(null);
82
- });
83
- expect(result.current.selectedRange).toBeNull();
84
- });
85
-
86
- it('onCopy writes to clipboard for single cell', async () => {
87
- const { result } = renderHook(() =>
88
- useCellClipboard({ data, columns }),
89
- );
90
- act(() => {
91
- result.current.setSelectedRange({ startRow: 0, startCol: 0, endRow: 0, endCol: 0 });
92
- });
93
- act(() => {
94
- result.current.onCopy();
95
- });
96
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Alice');
97
- });
98
-
99
- it('onCopy writes tab-separated values for range', () => {
100
- const { result } = renderHook(() =>
101
- useCellClipboard({ data, columns }),
102
- );
103
- act(() => {
104
- result.current.setSelectedRange({ startRow: 0, startCol: 0, endRow: 1, endCol: 2 });
105
- });
106
- act(() => {
107
- result.current.onCopy();
108
- });
109
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
110
- 'Alice\t30\tNYC\nBob\t25\tLA',
111
- );
112
- });
113
-
114
- it('onPaste reads from clipboard and calls callback', async () => {
115
- const onPaste = vi.fn();
116
- clipboardText = 'Dave\t40';
117
- const { result } = renderHook(() =>
118
- useCellClipboard({ data, columns, onPaste }),
119
- );
120
- act(() => {
121
- result.current.setSelectedRange({ startRow: 0, startCol: 0, endRow: 0, endCol: 1 });
122
- });
123
- await act(async () => {
124
- result.current.onPaste();
125
- // Let the clipboard promise resolve
126
- await Promise.resolve();
127
- });
128
- expect(onPaste).toHaveBeenCalledWith([
129
- { rowIndex: 0, field: 'name', value: 'Dave' },
130
- { rowIndex: 0, field: 'age', value: '40' },
131
- ]);
132
- });
133
- });
134
-
135
- // =========================================================================
136
- // useGradientColor
137
- // =========================================================================
138
- describe('useGradientColor', () => {
139
- const data = [
140
- { score: 0 },
141
- { score: 50 },
142
- { score: 100 },
143
- ];
144
-
145
- it('returns undefined when value is non-numeric', () => {
146
- const { result } = renderHook(() =>
147
- useGradientColor({ field: 'score', data }),
148
- );
149
- expect(result.current({ score: 'abc' })).toBeUndefined();
150
- });
151
-
152
- it('returns gradient class for min value', () => {
153
- const { result } = renderHook(() =>
154
- useGradientColor({ field: 'score', data }),
155
- );
156
- expect(result.current({ score: 0 })).toBe('bg-green-100');
157
- });
158
-
159
- it('returns gradient class for max value', () => {
160
- const { result } = renderHook(() =>
161
- useGradientColor({ field: 'score', data }),
162
- );
163
- expect(result.current({ score: 100 })).toBe('bg-red-100');
164
- });
165
-
166
- it('returns mid-range gradient', () => {
167
- const { result } = renderHook(() =>
168
- useGradientColor({ field: 'score', data }),
169
- );
170
- expect(result.current({ score: 50 })).toBe('bg-yellow-100');
171
- });
172
-
173
- it('handles empty data array', () => {
174
- const { result } = renderHook(() =>
175
- useGradientColor({ field: 'score', data: [] }),
176
- );
177
- // min === max === 0, so first stop returned
178
- expect(result.current({ score: 0 })).toBe('bg-green-100');
179
- });
180
-
181
- it('handles custom color stops', () => {
182
- const stops = [
183
- { position: 0, className: 'bg-blue-100' },
184
- { position: 1, className: 'bg-purple-100' },
185
- ];
186
- const { result } = renderHook(() =>
187
- useGradientColor({ field: 'score', data, stops }),
188
- );
189
- expect(result.current({ score: 0 })).toBe('bg-blue-100');
190
- expect(result.current({ score: 100 })).toBe('bg-purple-100');
191
- });
192
- });
193
-
194
- // =========================================================================
195
- // useGroupReorder
196
- // =========================================================================
197
- describe('useGroupReorder', () => {
198
- const groupKeys = ['alpha', 'beta', 'gamma'];
199
-
200
- it('initializes with default order', () => {
201
- const { result } = renderHook(() =>
202
- useGroupReorder({ groupKeys }),
203
- );
204
- expect(result.current.groupOrder).toEqual(['alpha', 'beta', 'gamma']);
205
- });
206
-
207
- it('moveGroup reorders correctly', () => {
208
- const { result } = renderHook(() =>
209
- useGroupReorder({ groupKeys }),
210
- );
211
- act(() => {
212
- result.current.moveGroup(0, 2);
213
- });
214
- expect(result.current.groupOrder).toEqual(['beta', 'gamma', 'alpha']);
215
- });
216
-
217
- it('handles drag start and end events', () => {
218
- const { result } = renderHook(() =>
219
- useGroupReorder({ groupKeys }),
220
- );
221
-
222
- const mockEvent = {
223
- dataTransfer: { effectAllowed: '', setData: vi.fn() },
224
- } as unknown as React.DragEvent;
225
-
226
- act(() => {
227
- result.current.onDragStart(mockEvent, 'beta');
228
- });
229
- expect(result.current.draggingKey).toBe('beta');
230
-
231
- act(() => {
232
- result.current.onDragEnd();
233
- });
234
- expect(result.current.draggingKey).toBeNull();
235
- });
236
-
237
- it('returns isDragging state', () => {
238
- const { result } = renderHook(() =>
239
- useGroupReorder({ groupKeys }),
240
- );
241
- // Initially not dragging
242
- expect(result.current.draggingKey).toBeNull();
243
-
244
- const mockEvent = {
245
- dataTransfer: { effectAllowed: '', setData: vi.fn() },
246
- } as unknown as React.DragEvent;
247
-
248
- act(() => {
249
- result.current.onDragStart(mockEvent, 'gamma');
250
- });
251
- expect(result.current.draggingKey).toBe('gamma');
252
- });
253
- });
254
-
255
- // =========================================================================
256
- // FormulaBar
257
- // =========================================================================
258
- describe('FormulaBar', () => {
259
- it('renders current cell value', () => {
260
- render(<FormulaBar value="Hello" />);
261
- const input = screen.getByRole('textbox');
262
- expect(input).toHaveValue('Hello');
263
- });
264
-
265
- it('enters edit mode on click', () => {
266
- render(<FormulaBar value="Hello" />);
267
- const input = screen.getByRole('textbox');
268
- expect(input).toHaveAttribute('readonly');
269
- fireEvent.click(input);
270
- expect(input).not.toHaveAttribute('readonly');
271
- });
272
-
273
- it('confirms on Enter key', () => {
274
- const onConfirm = vi.fn();
275
- render(<FormulaBar value="Hello" onConfirm={onConfirm} />);
276
- const input = screen.getByRole('textbox');
277
- fireEvent.click(input);
278
- fireEvent.change(input, { target: { value: 'World' } });
279
- fireEvent.keyDown(input, { key: 'Enter' });
280
- expect(onConfirm).toHaveBeenCalledWith('World');
281
- });
282
-
283
- it('cancels on Escape key', () => {
284
- const onCancel = vi.fn();
285
- render(<FormulaBar value="Hello" onCancel={onCancel} />);
286
- const input = screen.getByRole('textbox');
287
- fireEvent.click(input);
288
- fireEvent.keyDown(input, { key: 'Escape' });
289
- expect(onCancel).toHaveBeenCalled();
290
- });
291
-
292
- it('calls onConfirm with new value', () => {
293
- const onConfirm = vi.fn();
294
- render(<FormulaBar value="old" onConfirm={onConfirm} />);
295
- const input = screen.getByRole('textbox');
296
- fireEvent.click(input);
297
- fireEvent.change(input, { target: { value: 'new' } });
298
- fireEvent.keyDown(input, { key: 'Enter' });
299
- expect(onConfirm).toHaveBeenCalledWith('new');
300
- });
301
-
302
- it('calls onCancel', () => {
303
- const onCancel = vi.fn();
304
- render(<FormulaBar value="val" onCancel={onCancel} />);
305
- const input = screen.getByRole('textbox');
306
- fireEvent.click(input);
307
- fireEvent.keyDown(input, { key: 'Escape' });
308
- expect(onCancel).toHaveBeenCalledTimes(1);
309
- });
310
- });
311
-
312
- // =========================================================================
313
- // SplitPaneGrid
314
- // =========================================================================
315
- describe('SplitPaneGrid', () => {
316
- it('renders left and right panes', () => {
317
- render(
318
- <SplitPaneGrid
319
- frozenWidth={200}
320
- left={<div data-testid="left">Left</div>}
321
- right={<div data-testid="right">Right</div>}
322
- />,
323
- );
324
- expect(screen.getByTestId('left')).toBeInTheDocument();
325
- expect(screen.getByTestId('right')).toBeInTheDocument();
326
- });
327
-
328
- it('shows resize divider', () => {
329
- render(
330
- <SplitPaneGrid
331
- frozenWidth={200}
332
- left={<div>L</div>}
333
- right={<div>R</div>}
334
- />,
335
- );
336
- expect(screen.getByRole('separator')).toBeInTheDocument();
337
- });
338
-
339
- it('applies minWidth constraints via frozen pane style', () => {
340
- const { container } = render(
341
- <SplitPaneGrid
342
- frozenWidth={150}
343
- minFrozenWidth={100}
344
- minScrollableWidth={200}
345
- left={<div>L</div>}
346
- right={<div>R</div>}
347
- />,
348
- );
349
- // The frozen pane should have the specified width
350
- const frozenPane = container.querySelector('[style]');
351
- expect(frozenPane).toHaveStyle({ width: '150px' });
352
- });
353
- });
354
-
355
- // =========================================================================
356
- // useGroupedData – aggregations
357
- // =========================================================================
358
- describe('useGroupedData aggregations', () => {
359
- const data = [
360
- { category: 'A', amount: 10 },
361
- { category: 'A', amount: 20 },
362
- { category: 'B', amount: 30 },
363
- { category: 'B', amount: 40 },
364
- { category: 'B', amount: 50 },
365
- ];
366
-
367
- const config = {
368
- fields: [{ field: 'category', order: 'asc' as const, collapsed: false }],
369
- };
370
-
371
- it('computes sum aggregation per group', () => {
372
- const agg: AggregationConfig[] = [{ field: 'amount', type: 'sum' }];
373
- const { result } = renderHook(() => useGroupedData(config, data, agg));
374
-
375
- const groupA = result.current.groups.find((g) => g.key === 'A')!;
376
- const groupB = result.current.groups.find((g) => g.key === 'B')!;
377
-
378
- expect(groupA.aggregations[0]).toEqual({ field: 'amount', type: 'sum', value: 30 });
379
- expect(groupB.aggregations[0]).toEqual({ field: 'amount', type: 'sum', value: 120 });
380
- });
381
-
382
- it('computes count aggregation per group', () => {
383
- const agg: AggregationConfig[] = [{ field: 'amount', type: 'count' }];
384
- const { result } = renderHook(() => useGroupedData(config, data, agg));
385
-
386
- const groupA = result.current.groups.find((g) => g.key === 'A')!;
387
- const groupB = result.current.groups.find((g) => g.key === 'B')!;
388
-
389
- expect(groupA.aggregations[0]).toEqual({ field: 'amount', type: 'count', value: 2 });
390
- expect(groupB.aggregations[0]).toEqual({ field: 'amount', type: 'count', value: 3 });
391
- });
392
-
393
- it('computes avg aggregation per group', () => {
394
- const agg: AggregationConfig[] = [{ field: 'amount', type: 'avg' }];
395
- const { result } = renderHook(() => useGroupedData(config, data, agg));
396
-
397
- const groupA = result.current.groups.find((g) => g.key === 'A')!;
398
- const groupB = result.current.groups.find((g) => g.key === 'B')!;
399
-
400
- expect(groupA.aggregations[0]).toEqual({ field: 'amount', type: 'avg', value: 15 });
401
- expect(groupB.aggregations[0]).toEqual({ field: 'amount', type: 'avg', value: 40 });
402
- });
403
-
404
- it('handles multiple aggregation types simultaneously', () => {
405
- const agg: AggregationConfig[] = [
406
- { field: 'amount', type: 'sum' },
407
- { field: 'amount', type: 'count' },
408
- { field: 'amount', type: 'avg' },
409
- ];
410
- const { result } = renderHook(() => useGroupedData(config, data, agg));
411
-
412
- const groupA = result.current.groups.find((g) => g.key === 'A')!;
413
- expect(groupA.aggregations).toHaveLength(3);
414
- expect(groupA.aggregations[0]).toEqual({ field: 'amount', type: 'sum', value: 30 });
415
- expect(groupA.aggregations[1]).toEqual({ field: 'amount', type: 'count', value: 2 });
416
- expect(groupA.aggregations[2]).toEqual({ field: 'amount', type: 'avg', value: 15 });
417
- });
418
- });