@object-ui/plugin-grid 3.0.3 → 3.1.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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +12 -0
- package/dist/index.js +2173 -922
- package/dist/index.umd.cjs +9 -3
- package/dist/plugin-grid/src/FormulaBar.d.ts +29 -0
- package/dist/plugin-grid/src/GroupRow.d.ts +23 -0
- package/dist/plugin-grid/src/ImportWizard.d.ts +29 -0
- package/dist/plugin-grid/src/ObjectGrid.d.ts +1 -0
- package/dist/plugin-grid/src/SplitPaneGrid.d.ts +22 -0
- package/dist/plugin-grid/src/components/BulkActionBar.d.ts +12 -0
- package/dist/plugin-grid/src/components/RowActionMenu.d.ts +23 -0
- package/dist/plugin-grid/src/index.d.ts +22 -2
- package/dist/plugin-grid/src/useCellClipboard.d.ts +47 -0
- package/dist/plugin-grid/src/useColumnSummary.d.ts +25 -0
- package/dist/plugin-grid/src/useGradientColor.d.ts +37 -0
- package/dist/plugin-grid/src/useGroupReorder.d.ts +34 -0
- package/dist/plugin-grid/src/useGroupedData.d.ts +24 -3
- package/package.json +10 -10
- package/src/FormulaBar.tsx +151 -0
- package/src/GroupRow.tsx +69 -0
- package/src/ImportWizard.tsx +412 -0
- package/src/ListColumnExtensions.test.tsx +4 -5
- package/src/ObjectGrid.tsx +1002 -139
- package/src/SplitPaneGrid.tsx +120 -0
- package/src/VirtualGrid.tsx +2 -2
- package/src/__tests__/GroupRow.test.tsx +206 -0
- package/src/__tests__/ImportPreview.test.tsx +171 -0
- package/src/__tests__/accessorKey-inference.test.tsx +132 -0
- package/src/__tests__/airtable-style.test.tsx +508 -0
- package/src/__tests__/column-features.test.tsx +490 -0
- package/src/__tests__/grid-export.test.tsx +121 -0
- package/src/__tests__/mobile-card-view.test.tsx +355 -0
- package/src/__tests__/objectdef-enrichment.test.tsx +566 -0
- package/src/__tests__/phase11-features.test.tsx +418 -0
- package/src/__tests__/row-bulk-actions.test.tsx +413 -0
- package/src/__tests__/row-height.test.tsx +160 -0
- package/src/__tests__/useGroupedData.test.ts +165 -0
- package/src/components/BulkActionBar.tsx +66 -0
- package/src/components/RowActionMenu.tsx +91 -0
- package/src/index.tsx +46 -2
- package/src/useCellClipboard.ts +136 -0
- package/src/useColumnSummary.ts +128 -0
- package/src/useGradientColor.ts +103 -0
- package/src/useGroupReorder.ts +123 -0
- package/src/useGroupedData.ts +69 -4
|
@@ -0,0 +1,418 @@
|
|
|
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
|
+
});
|