@opencorestack/opengridx 2.1.0 → 3.0.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/CHANGELOG.md +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -166
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +6 -7
- package/docs/components/datagrid.md +15 -13
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +90 -38
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +126 -39
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +19 -4
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +279 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +104 -5
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +17 -5
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +82 -51
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +55 -5
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { renderHook, act } from '@testing-library/react';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { useGridKeyboardNavigation } from './useGridKeyboardNavigation';
|
|
5
|
-
|
|
6
|
-
// Helper: build a minimal React.KeyboardEvent mock
|
|
7
|
-
function keyEvent(key: string): React.KeyboardEvent {
|
|
8
|
-
return {
|
|
9
|
-
key,
|
|
10
|
-
preventDefault: vi.fn(),
|
|
11
|
-
stopPropagation: vi.fn(),
|
|
12
|
-
nativeEvent: new KeyboardEvent('keydown', { key }),
|
|
13
|
-
isDefaultPrevented: () => false,
|
|
14
|
-
isPropagationStopped: () => false,
|
|
15
|
-
persist: vi.fn(),
|
|
16
|
-
bubbles: true,
|
|
17
|
-
cancelable: true,
|
|
18
|
-
defaultPrevented: false,
|
|
19
|
-
eventPhase: 0,
|
|
20
|
-
isTrusted: false,
|
|
21
|
-
timeStamp: 0,
|
|
22
|
-
type: 'keydown',
|
|
23
|
-
currentTarget: null,
|
|
24
|
-
target: null,
|
|
25
|
-
shiftKey: false,
|
|
26
|
-
ctrlKey: false,
|
|
27
|
-
metaKey: false,
|
|
28
|
-
altKey: false,
|
|
29
|
-
} as unknown as React.KeyboardEvent;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const ROWS = [
|
|
33
|
-
{ id: 1, name: 'Alice' },
|
|
34
|
-
{ id: 2, name: 'Bob' },
|
|
35
|
-
{ id: 3, name: 'Carol' },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const NAV_COLS = [
|
|
39
|
-
{ field: 'name' },
|
|
40
|
-
{ field: 'age' },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const MOCK_EDITING = {
|
|
44
|
-
editingCell: null,
|
|
45
|
-
startCellEdit: vi.fn(),
|
|
46
|
-
stopCellEdit: vi.fn(),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const MOCK_VIRTUALIZATION = {
|
|
50
|
-
cumulativeHeights: [52, 104, 156],
|
|
51
|
-
pinnedTopHeight: 0,
|
|
52
|
-
columnMetrics: null,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const BASE_PARAMS = {
|
|
56
|
-
allRenderableRows: ROWS,
|
|
57
|
-
navigationColumns: NAV_COLS,
|
|
58
|
-
checkboxSelection: false,
|
|
59
|
-
selectedRowIds: new Set<number | string>(),
|
|
60
|
-
handleSelectionChange: vi.fn(),
|
|
61
|
-
handleDetailPanelToggle: vi.fn(),
|
|
62
|
-
editingHandlers: MOCK_EDITING,
|
|
63
|
-
setKeyboardMode: vi.fn(),
|
|
64
|
-
sortModel: [],
|
|
65
|
-
handleSort: vi.fn(),
|
|
66
|
-
isCellEditable: undefined,
|
|
67
|
-
pagination: false,
|
|
68
|
-
pageSize: 100,
|
|
69
|
-
virtualization: MOCK_VIRTUALIZATION,
|
|
70
|
-
viewportRef: { current: null } as React.RefObject<HTMLDivElement | null>,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
describe('useGridKeyboardNavigation — arrow keys', () => {
|
|
74
|
-
it('ArrowDown moves focusedCell to the next row', () => {
|
|
75
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
76
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
|
|
77
|
-
act(() => { result.current.handleKeyDown(keyEvent('ArrowDown')); });
|
|
78
|
-
expect(result.current.focusedCell).toEqual({ id: 2, field: 'name' });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('ArrowUp at header row does not move above it', () => {
|
|
82
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
83
|
-
// HEADER is the topmost position; ArrowUp from there should not move
|
|
84
|
-
act(() => { result.current.setFocusedCell({ id: 'HEADER', field: 'name' }); });
|
|
85
|
-
act(() => { result.current.handleKeyDown(keyEvent('ArrowUp')); });
|
|
86
|
-
expect(result.current.focusedCell?.id).toBe('HEADER');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('ArrowDown at last row — no change', () => {
|
|
90
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
91
|
-
// ROWS has 3 entries; last row id is 3
|
|
92
|
-
const lastRow = ROWS[ROWS.length - 1];
|
|
93
|
-
act(() => { result.current.setFocusedCell({ id: lastRow.id, field: 'name' }); });
|
|
94
|
-
act(() => { result.current.handleKeyDown(keyEvent('ArrowDown')); });
|
|
95
|
-
expect(result.current.focusedCell?.id).toBe(lastRow.id);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('ArrowRight moves focusedCell to next column', () => {
|
|
99
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
100
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
|
|
101
|
-
act(() => { result.current.handleKeyDown(keyEvent('ArrowRight')); });
|
|
102
|
-
expect(result.current.focusedCell?.field).toBe('age');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('ArrowLeft at header first column does not move left', () => {
|
|
106
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
107
|
-
// HEADER + first col is the leftmost position; wrap-left would require r=-2 which is out of bounds
|
|
108
|
-
act(() => { result.current.setFocusedCell({ id: 'HEADER', field: 'name' }); });
|
|
109
|
-
act(() => { result.current.handleKeyDown(keyEvent('ArrowLeft')); });
|
|
110
|
-
expect(result.current.focusedCell?.field).toBe('name');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('useGridKeyboardNavigation — Home / End', () => {
|
|
115
|
-
it('Home moves to first column', () => {
|
|
116
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
117
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'age' }); });
|
|
118
|
-
act(() => { result.current.handleKeyDown(keyEvent('Home')); });
|
|
119
|
-
expect(result.current.focusedCell?.field).toBe('name');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('End moves to last column', () => {
|
|
123
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(BASE_PARAMS));
|
|
124
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
|
|
125
|
-
act(() => { result.current.handleKeyDown(keyEvent('End')); });
|
|
126
|
-
expect(result.current.focusedCell?.field).toBe('age');
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('useGridKeyboardNavigation — editing', () => {
|
|
131
|
-
it('Enter calls startCellEdit on an editable cell', () => {
|
|
132
|
-
const startCellEdit = vi.fn();
|
|
133
|
-
const params = {
|
|
134
|
-
...BASE_PARAMS,
|
|
135
|
-
editingHandlers: { ...MOCK_EDITING, startCellEdit },
|
|
136
|
-
navigationColumns: [{ field: 'name', editable: true }],
|
|
137
|
-
};
|
|
138
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(params));
|
|
139
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
|
|
140
|
-
act(() => { result.current.handleKeyDown(keyEvent('Enter')); });
|
|
141
|
-
expect(startCellEdit).toHaveBeenCalledWith(
|
|
142
|
-
expect.objectContaining({ id: 1, field: 'name' })
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('Escape calls stopCellEdit with cancel=true when editing', () => {
|
|
147
|
-
const stopCellEdit = vi.fn();
|
|
148
|
-
const params = {
|
|
149
|
-
...BASE_PARAMS,
|
|
150
|
-
editingHandlers: {
|
|
151
|
-
editingCell: { id: 1, field: 'name', value: 'Alice' },
|
|
152
|
-
startCellEdit: vi.fn(),
|
|
153
|
-
stopCellEdit,
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
const { result } = renderHook(() => useGridKeyboardNavigation(params));
|
|
157
|
-
act(() => { result.current.setFocusedCell({ id: 1, field: 'name' }); });
|
|
158
|
-
act(() => { result.current.handleKeyDown(keyEvent('Escape')); });
|
|
159
|
-
expect(stopCellEdit).toHaveBeenCalledWith({ cancel: true });
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('useGridKeyboardNavigation — selection', () => {
|
|
164
|
-
it('Space toggles checkbox selection on focused row', () => {
|
|
165
|
-
const handleSelectionChange = vi.fn();
|
|
166
|
-
const { result } = renderHook(() => useGridKeyboardNavigation({
|
|
167
|
-
...BASE_PARAMS,
|
|
168
|
-
checkboxSelection: true,
|
|
169
|
-
handleSelectionChange,
|
|
170
|
-
}));
|
|
171
|
-
// Space only fires handleSelectionChange when field === '__checkbox_col__'
|
|
172
|
-
act(() => { result.current.setFocusedCell({ id: 2, field: '__checkbox_col__' }); });
|
|
173
|
-
act(() => { result.current.handleKeyDown(keyEvent(' ')); });
|
|
174
|
-
expect(handleSelectionChange).toHaveBeenCalledWith(2, true);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderHook } from '@testing-library/react';
|
|
3
|
-
import { useGridRowPipeline } from './useGridRowPipeline';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_PAGINATION = { page: 0, pageSize: 3 };
|
|
6
|
-
|
|
7
|
-
const ROWS = [
|
|
8
|
-
{ id: 1, name: 'Alice', age: 30 },
|
|
9
|
-
{ id: 2, name: 'Bob', age: 25 },
|
|
10
|
-
{ id: 3, name: 'Carol', age: 35 },
|
|
11
|
-
{ id: 4, name: 'Dan', age: 28 },
|
|
12
|
-
{ id: 5, name: 'Eve', age: 22 },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const BASE_PARAMS = {
|
|
16
|
-
effectiveRows: ROWS,
|
|
17
|
-
activeHierarchyHandlers: null,
|
|
18
|
-
filterMode: 'client',
|
|
19
|
-
filterModel: { items: [] },
|
|
20
|
-
sortModel: [],
|
|
21
|
-
sortingMode: 'client',
|
|
22
|
-
pagination: false,
|
|
23
|
-
paginationMode: 'client',
|
|
24
|
-
effectivePaginationModel: DEFAULT_PAGINATION,
|
|
25
|
-
isLoading: false,
|
|
26
|
-
pageSize: 3,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
describe('useGridRowPipeline — filtering', () => {
|
|
30
|
-
it('passes all rows through when filterModel is empty', () => {
|
|
31
|
-
const { result } = renderHook(() => useGridRowPipeline(BASE_PARAMS));
|
|
32
|
-
expect(result.current.filteredRows).toHaveLength(5);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('filters rows by string contains operator', () => {
|
|
36
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
37
|
-
...BASE_PARAMS,
|
|
38
|
-
filterModel: { items: [{ id: 'f1', field: 'name', operator: 'contains' as const, value: 'al' }] },
|
|
39
|
-
}));
|
|
40
|
-
// 'Alice' contains 'al' (case-insensitive); 'Carol' does not ('car-o-l')
|
|
41
|
-
expect(result.current.filteredRows.map(r => r.id)).toEqual([1]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('filterMode=server passes rows through unfiltered', () => {
|
|
45
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
46
|
-
...BASE_PARAMS,
|
|
47
|
-
filterMode: 'server',
|
|
48
|
-
// dataSource required: hook only bypasses filtering when both filterMode=server AND dataSource are set
|
|
49
|
-
dataSource: {} as never,
|
|
50
|
-
filterModel: { items: [{ id: 'f1', field: 'name', operator: 'contains' as const, value: 'Alice' }] },
|
|
51
|
-
}));
|
|
52
|
-
expect(result.current.filteredRows).toHaveLength(5);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('useGridRowPipeline — sorting', () => {
|
|
57
|
-
it('sorts rows ascending by field', () => {
|
|
58
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
59
|
-
...BASE_PARAMS,
|
|
60
|
-
sortModel: [{ field: 'age', sort: 'asc' }],
|
|
61
|
-
}));
|
|
62
|
-
const ages = result.current.sortedUnpinnedRows.map(r => r.age);
|
|
63
|
-
expect(ages).toEqual([22, 25, 28, 30, 35]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('sorts rows descending by field', () => {
|
|
67
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
68
|
-
...BASE_PARAMS,
|
|
69
|
-
sortModel: [{ field: 'age', sort: 'desc' }],
|
|
70
|
-
}));
|
|
71
|
-
const ages = result.current.sortedUnpinnedRows.map(r => r.age);
|
|
72
|
-
expect(ages).toEqual([35, 30, 28, 25, 22]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('sortingMode=server passes rows through unsorted', () => {
|
|
76
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
77
|
-
...BASE_PARAMS,
|
|
78
|
-
sortingMode: 'server',
|
|
79
|
-
// dataSource required: hook only bypasses sorting when both sortingMode=server AND dataSource are set
|
|
80
|
-
dataSource: {} as never,
|
|
81
|
-
sortModel: [{ field: 'age', sort: 'asc' }],
|
|
82
|
-
}));
|
|
83
|
-
// Rows stay in original order
|
|
84
|
-
expect(result.current.sortedUnpinnedRows.map(r => r.id)).toEqual([1, 2, 3, 4, 5]);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('useGridRowPipeline — pagination', () => {
|
|
89
|
-
it('returns first pageSize rows on page 0', () => {
|
|
90
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
91
|
-
...BASE_PARAMS,
|
|
92
|
-
pagination: true,
|
|
93
|
-
effectivePaginationModel: { page: 0, pageSize: 3 },
|
|
94
|
-
}));
|
|
95
|
-
expect(result.current.paginatedUnpinnedRows.map(r => r.id)).toEqual([1, 2, 3]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('returns correct offset slice on page 1', () => {
|
|
99
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
100
|
-
...BASE_PARAMS,
|
|
101
|
-
pagination: true,
|
|
102
|
-
effectivePaginationModel: { page: 1, pageSize: 3 },
|
|
103
|
-
}));
|
|
104
|
-
expect(result.current.paginatedUnpinnedRows.map(r => r.id)).toEqual([4, 5]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('paginationMode=server returns all rows unsliced', () => {
|
|
108
|
-
const { result } = renderHook(() => useGridRowPipeline({
|
|
109
|
-
...BASE_PARAMS,
|
|
110
|
-
pagination: true,
|
|
111
|
-
paginationMode: 'server',
|
|
112
|
-
dataSource: {} as never,
|
|
113
|
-
effectivePaginationModel: { page: 0, pageSize: 2 },
|
|
114
|
-
}));
|
|
115
|
-
expect(result.current.paginatedUnpinnedRows).toHaveLength(5);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderHook } from '@testing-library/react';
|
|
3
|
-
import { useAggregation, formatAggregationValue } from './useAggregation';
|
|
4
|
-
|
|
5
|
-
const ROWS = [
|
|
6
|
-
{ id: 1, salary: 50000, dept: 'Eng', active: true },
|
|
7
|
-
{ id: 2, salary: 60000, dept: 'Eng', active: false },
|
|
8
|
-
{ id: 3, salary: 70000, dept: 'HR', active: true },
|
|
9
|
-
{ id: 4, salary: null, dept: 'HR', active: true },
|
|
10
|
-
{ id: 5, salary: 80000, dept: 'Eng', active: null },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
describe('formatAggregationValue', () => {
|
|
14
|
-
it('returns em-dash for null', () => {
|
|
15
|
-
expect(formatAggregationValue(null, 'sum')).toBe('—');
|
|
16
|
-
});
|
|
17
|
-
it('formats avg with up to 2 decimal places', () => {
|
|
18
|
-
expect(formatAggregationValue(1234.5678, 'avg')).toBe('1,234.57');
|
|
19
|
-
});
|
|
20
|
-
it('formats non-avg numbers without decimals', () => {
|
|
21
|
-
expect(formatAggregationValue(1234567, 'sum')).toBe('1,234,567');
|
|
22
|
-
});
|
|
23
|
-
it('converts non-numeric to string', () => {
|
|
24
|
-
expect(formatAggregationValue('Engineering', 'unique')).toBe('Engineering');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('useAggregation — client-side', () => {
|
|
29
|
-
it('returns empty result when no aggregation model', () => {
|
|
30
|
-
const { result } = renderHook(() =>
|
|
31
|
-
useAggregation({ rows: ROWS, aggregationModel: {}, isServerSide: false })
|
|
32
|
-
);
|
|
33
|
-
expect(result.current.aggregationResult).toEqual({});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('computes sum correctly, skipping nulls', () => {
|
|
37
|
-
const { result } = renderHook(() =>
|
|
38
|
-
useAggregation({
|
|
39
|
-
rows: ROWS,
|
|
40
|
-
aggregationModel: { salary: 'sum' },
|
|
41
|
-
isServerSide: false,
|
|
42
|
-
})
|
|
43
|
-
);
|
|
44
|
-
expect(result.current.aggregationResult.salary).toBe(260000);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('computes avg correctly', () => {
|
|
48
|
-
const { result } = renderHook(() =>
|
|
49
|
-
useAggregation({
|
|
50
|
-
rows: ROWS,
|
|
51
|
-
aggregationModel: { salary: 'avg' },
|
|
52
|
-
isServerSide: false,
|
|
53
|
-
})
|
|
54
|
-
);
|
|
55
|
-
expect(result.current.aggregationResult.salary).toBeCloseTo(65000);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('computes count of non-null values', () => {
|
|
59
|
-
const { result } = renderHook(() =>
|
|
60
|
-
useAggregation({
|
|
61
|
-
rows: ROWS,
|
|
62
|
-
aggregationModel: { salary: 'count' },
|
|
63
|
-
isServerSide: false,
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
expect(result.current.aggregationResult.salary).toBe(4);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('computes min and max', () => {
|
|
70
|
-
const { result } = renderHook(() =>
|
|
71
|
-
useAggregation({
|
|
72
|
-
rows: ROWS,
|
|
73
|
-
aggregationModel: { salary: 'min' },
|
|
74
|
-
isServerSide: false,
|
|
75
|
-
})
|
|
76
|
-
);
|
|
77
|
-
expect(result.current.aggregationResult.salary).toBe(50000);
|
|
78
|
-
|
|
79
|
-
const { result: r2 } = renderHook(() =>
|
|
80
|
-
useAggregation({
|
|
81
|
-
rows: ROWS,
|
|
82
|
-
aggregationModel: { salary: 'max' },
|
|
83
|
-
isServerSide: false,
|
|
84
|
-
})
|
|
85
|
-
);
|
|
86
|
-
expect(r2.current.aggregationResult.salary).toBe(80000);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('computes unique count', () => {
|
|
90
|
-
const { result } = renderHook(() =>
|
|
91
|
-
useAggregation({
|
|
92
|
-
rows: ROWS,
|
|
93
|
-
aggregationModel: { dept: 'unique' },
|
|
94
|
-
isServerSide: false,
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
expect(result.current.aggregationResult.dept).toBe(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('handles unknown function gracefully — skips the field', () => {
|
|
101
|
-
const { result } = renderHook(() =>
|
|
102
|
-
useAggregation({
|
|
103
|
-
rows: ROWS,
|
|
104
|
-
aggregationModel: { salary: 'median' as 'sum' },
|
|
105
|
-
isServerSide: false,
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
expect(result.current.aggregationResult.salary).toBeUndefined();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('returns {} and does not mutate state when isServerSide=true', () => {
|
|
112
|
-
const { result } = renderHook(() =>
|
|
113
|
-
useAggregation({
|
|
114
|
-
rows: ROWS,
|
|
115
|
-
aggregationModel: { salary: 'sum' },
|
|
116
|
-
isServerSide: true,
|
|
117
|
-
})
|
|
118
|
-
);
|
|
119
|
-
// No dataSource → serverResult stays {}
|
|
120
|
-
expect(result.current.aggregationResult).toEqual({});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('returns serverAggregationResults when provided', () => {
|
|
124
|
-
const serverResults = { salary: 999999 };
|
|
125
|
-
const { result } = renderHook(() =>
|
|
126
|
-
useAggregation({
|
|
127
|
-
rows: ROWS,
|
|
128
|
-
aggregationModel: { salary: 'sum' },
|
|
129
|
-
isServerSide: true,
|
|
130
|
-
serverAggregationResults: serverResults,
|
|
131
|
-
})
|
|
132
|
-
);
|
|
133
|
-
expect(result.current.aggregationResult).toEqual(serverResults);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('skips aggregation when function not in availableAggregationFunctions', () => {
|
|
137
|
-
const columns = [{ field: 'salary', availableAggregationFunctions: ['avg', 'count'] }];
|
|
138
|
-
const { result } = renderHook(() =>
|
|
139
|
-
useAggregation({
|
|
140
|
-
rows: ROWS,
|
|
141
|
-
columns,
|
|
142
|
-
aggregationModel: { salary: 'sum' },
|
|
143
|
-
isServerSide: false,
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
// 'sum' is not in the allowed list, so the result should be absent
|
|
147
|
-
expect(result.current.aggregationResult.salary).toBeUndefined();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('computes aggregation when function is in availableAggregationFunctions', () => {
|
|
151
|
-
const columns = [{ field: 'salary', availableAggregationFunctions: ['sum', 'avg'] }];
|
|
152
|
-
const { result } = renderHook(() =>
|
|
153
|
-
useAggregation({
|
|
154
|
-
rows: ROWS,
|
|
155
|
-
columns,
|
|
156
|
-
aggregationModel: { salary: 'sum' },
|
|
157
|
-
isServerSide: false,
|
|
158
|
-
})
|
|
159
|
-
);
|
|
160
|
-
expect(result.current.aggregationResult.salary).toBe(260000);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('does not restrict aggregation when availableAggregationFunctions is absent', () => {
|
|
164
|
-
const columns = [{ field: 'salary' }];
|
|
165
|
-
const { result } = renderHook(() =>
|
|
166
|
-
useAggregation({
|
|
167
|
-
rows: ROWS,
|
|
168
|
-
columns,
|
|
169
|
-
aggregationModel: { salary: 'sum' },
|
|
170
|
-
isServerSide: false,
|
|
171
|
-
})
|
|
172
|
-
);
|
|
173
|
-
expect(result.current.aggregationResult.salary).toBe(260000);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for usePivot — including regression tests for grand total correctness.
|
|
3
|
-
*
|
|
4
|
-
* Bug (fixed): avg/min/max grand total was computed by re-aggregating
|
|
5
|
-
* per-row pivot values instead of re-computing from the raw source rows.
|
|
6
|
-
* This produced wrong results whenever groups had unequal sizes.
|
|
7
|
-
*
|
|
8
|
-
* Example of the failure:
|
|
9
|
-
* Group A: avg salary = (30k + 50k) / 2 = 40k
|
|
10
|
-
* Group B: avg salary = (90k) / 1 = 90k
|
|
11
|
-
* Summing those per-row averages → 130k / 2 = 65k ← WRONG
|
|
12
|
-
* Correct grand avg → (30k + 50k + 90k) / 3 = 56.67k
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect } from 'vitest';
|
|
16
|
-
import { renderHook } from '@testing-library/react';
|
|
17
|
-
import { usePivot } from './usePivot';
|
|
18
|
-
import type { GridColDef } from '../../types';
|
|
19
|
-
import type { GridPivotModel } from '../../types';
|
|
20
|
-
|
|
21
|
-
const COLS: GridColDef[] = [
|
|
22
|
-
{ field: 'dept', headerName: 'Department', width: 120 },
|
|
23
|
-
{ field: 'region', headerName: 'Region', width: 120 },
|
|
24
|
-
{ field: 'salary', headerName: 'Salary', width: 120, type: 'number' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
// Intentionally uneven groups so naive "average of averages" gives wrong results.
|
|
28
|
-
//
|
|
29
|
-
// dept=Eng → salaries [30000, 50000] → avg=40000, min=30000, max=50000
|
|
30
|
-
// dept=HR → salaries [90000] → avg=90000, min=90000, max=90000
|
|
31
|
-
//
|
|
32
|
-
// Grand total correct:
|
|
33
|
-
// avg = (30000+50000+90000)/3 ≈ 56666.67
|
|
34
|
-
// min = 30000
|
|
35
|
-
// max = 90000
|
|
36
|
-
const ROWS = [
|
|
37
|
-
{ id: 1, dept: 'Eng', region: 'US', salary: 30000 },
|
|
38
|
-
{ id: 2, dept: 'Eng', region: 'US', salary: 50000 },
|
|
39
|
-
{ id: 3, dept: 'HR', region: 'US', salary: 90000 },
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
const MODEL_NO_COL_FIELDS: GridPivotModel = {
|
|
43
|
-
rowFields: ['dept'],
|
|
44
|
-
columnFields: [],
|
|
45
|
-
valueFields: [
|
|
46
|
-
{ field: 'salary', aggFn: 'sum' },
|
|
47
|
-
{ field: 'salary', aggFn: 'avg' },
|
|
48
|
-
{ field: 'salary', aggFn: 'count' },
|
|
49
|
-
{ field: 'salary', aggFn: 'min' },
|
|
50
|
-
{ field: 'salary', aggFn: 'max' },
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function grandTotalRow(pivotRows: ReturnType<typeof usePivot>['pivotRows']) {
|
|
55
|
-
return pivotRows.find(r => r.id === '__pivot_grand_total__')!;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
-
// Basic functionality
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
describe('usePivot — basic', () => {
|
|
63
|
-
it('returns isValid=false when disabled', () => {
|
|
64
|
-
const { result } = renderHook(() =>
|
|
65
|
-
usePivot(ROWS, COLS, MODEL_NO_COL_FIELDS, false)
|
|
66
|
-
);
|
|
67
|
-
expect(result.current.isValid).toBe(false);
|
|
68
|
-
expect(result.current.pivotRows).toHaveLength(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('returns isValid=false when valueFields is empty', () => {
|
|
72
|
-
const { result } = renderHook(() =>
|
|
73
|
-
usePivot(ROWS, COLS, { ...MODEL_NO_COL_FIELDS, valueFields: [] }, true)
|
|
74
|
-
);
|
|
75
|
-
expect(result.current.isValid).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('produces one pivot row per unique rowField value plus grand total', () => {
|
|
79
|
-
const { result } = renderHook(() =>
|
|
80
|
-
usePivot(ROWS, COLS, MODEL_NO_COL_FIELDS, true)
|
|
81
|
-
);
|
|
82
|
-
// 2 groups (Eng, HR) + 1 grand total
|
|
83
|
-
expect(result.current.pivotRows).toHaveLength(3);
|
|
84
|
-
expect(result.current.isValid).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('computes per-group sum correctly', () => {
|
|
88
|
-
const model: GridPivotModel = {
|
|
89
|
-
...MODEL_NO_COL_FIELDS,
|
|
90
|
-
valueFields: [{ field: 'salary', aggFn: 'sum' }],
|
|
91
|
-
};
|
|
92
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
93
|
-
const rows = result.current.pivotRows;
|
|
94
|
-
const eng = rows.find(r => r.dept === 'Eng')!;
|
|
95
|
-
const hr = rows.find(r => r.dept === 'HR')!;
|
|
96
|
-
|
|
97
|
-
expect(eng['salarysum']).toBe(80000);
|
|
98
|
-
expect(hr['salarysum']).toBe(90000);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
-
// Regression: grand total avg/min/max
|
|
104
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
describe('usePivot — grand total regression', () => {
|
|
107
|
-
it('grand total sum = sum of all raw values', () => {
|
|
108
|
-
const model: GridPivotModel = {
|
|
109
|
-
...MODEL_NO_COL_FIELDS,
|
|
110
|
-
valueFields: [{ field: 'salary', aggFn: 'sum' }],
|
|
111
|
-
};
|
|
112
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
113
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
114
|
-
expect(gt['salarysum']).toBe(170000);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('grand total avg = raw mean, NOT average of per-group averages', () => {
|
|
118
|
-
// The naive bug: (40000 + 90000) / 2 = 65000
|
|
119
|
-
// The correct value: (30000 + 50000 + 90000) / 3 ≈ 56666.67
|
|
120
|
-
const model: GridPivotModel = {
|
|
121
|
-
...MODEL_NO_COL_FIELDS,
|
|
122
|
-
valueFields: [{ field: 'salary', aggFn: 'avg' }],
|
|
123
|
-
};
|
|
124
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
125
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
126
|
-
|
|
127
|
-
const correct = (30000 + 50000 + 90000) / 3;
|
|
128
|
-
const bugged = (40000 + 90000) / 2;
|
|
129
|
-
|
|
130
|
-
expect(gt['salaryavg']).toBeCloseTo(correct, 2);
|
|
131
|
-
expect(gt['salaryavg']).not.toBeCloseTo(bugged, 2);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('grand total min = minimum across ALL raw rows, not minimum of per-group minimums', () => {
|
|
135
|
-
// With equal-size groups the two approaches coincidentally match.
|
|
136
|
-
// This dataset has unequal groups, so we must use raw data.
|
|
137
|
-
const model: GridPivotModel = {
|
|
138
|
-
...MODEL_NO_COL_FIELDS,
|
|
139
|
-
valueFields: [{ field: 'salary', aggFn: 'min' }],
|
|
140
|
-
};
|
|
141
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
142
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
143
|
-
expect(gt['salarymin']).toBe(30000);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('grand total max = maximum across ALL raw rows', () => {
|
|
147
|
-
const model: GridPivotModel = {
|
|
148
|
-
...MODEL_NO_COL_FIELDS,
|
|
149
|
-
valueFields: [{ field: 'salary', aggFn: 'max' }],
|
|
150
|
-
};
|
|
151
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
152
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
153
|
-
expect(gt['salarymax']).toBe(90000);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('grand total count = total number of non-null raw values', () => {
|
|
157
|
-
const model: GridPivotModel = {
|
|
158
|
-
...MODEL_NO_COL_FIELDS,
|
|
159
|
-
valueFields: [{ field: 'salary', aggFn: 'count' }],
|
|
160
|
-
};
|
|
161
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
162
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
163
|
-
// sum of per-group counts = 2 + 1 = 3, which also equals total count here.
|
|
164
|
-
expect(gt['salarycount']).toBe(3);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('grand total avg ignores null salary values', () => {
|
|
168
|
-
const rowsWithNull = [
|
|
169
|
-
...ROWS,
|
|
170
|
-
{ id: 4, dept: 'Eng', region: 'US', salary: null as unknown as number },
|
|
171
|
-
];
|
|
172
|
-
const model: GridPivotModel = {
|
|
173
|
-
...MODEL_NO_COL_FIELDS,
|
|
174
|
-
valueFields: [{ field: 'salary', aggFn: 'avg' }],
|
|
175
|
-
};
|
|
176
|
-
const { result } = renderHook(() => usePivot(rowsWithNull, COLS, model, true));
|
|
177
|
-
const gt = grandTotalRow(result.current.pivotRows);
|
|
178
|
-
|
|
179
|
-
// null must be excluded: (30000+50000+90000)/3 not /4
|
|
180
|
-
const correct = (30000 + 50000 + 90000) / 3;
|
|
181
|
-
expect(gt['salaryavg']).toBeCloseTo(correct, 2);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
-
// Pivot with column fields (colKeys drive dynamic column generation)
|
|
187
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
describe('usePivot — with columnFields', () => {
|
|
190
|
-
it('produces one pivot column per unique columnField value × valueField', () => {
|
|
191
|
-
const model: GridPivotModel = {
|
|
192
|
-
rowFields: ['dept'],
|
|
193
|
-
columnFields: ['region'],
|
|
194
|
-
valueFields: [{ field: 'salary', aggFn: 'sum' }],
|
|
195
|
-
};
|
|
196
|
-
const { result } = renderHook(() => usePivot(ROWS, COLS, model, true));
|
|
197
|
-
// colKeys = ['US'] → 1 col per valueField = 1 column (+ dept label column)
|
|
198
|
-
expect(result.current.colKeys).toEqual(['US']);
|
|
199
|
-
// Total pivot columns = 1 row-field col + 1 value col
|
|
200
|
-
expect(result.current.pivotColumns).toHaveLength(2);
|
|
201
|
-
});
|
|
202
|
-
});
|