@opencorestack/opengridx 2.0.4 → 3.0.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/CHANGELOG.md +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- 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 -161
- 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 +45 -22
- package/docs/components/datagrid.md +14 -12
- 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 +100 -36
- 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 +147 -41
- 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 +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -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 +11 -0
- 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 +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- 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 +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- 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 +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- 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 +15 -3
- 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 +284 -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 +123 -0
- 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 +452 -250
- 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 +20 -7
- 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 +86 -90
- 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 +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -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 +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- 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 +344 -121
- 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 +78 -0
- 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 +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- 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/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 -137
- 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,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
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderHook } from '@testing-library/react';
|
|
3
|
-
import { useRowGrouping } from './useRowGrouping';
|
|
4
|
-
|
|
5
|
-
const ROWS = [
|
|
6
|
-
{ id: 1, dept: 'Engineering', name: 'Alice' },
|
|
7
|
-
{ id: 2, dept: 'Engineering', name: 'Bob' },
|
|
8
|
-
{ id: 3, dept: 'HR', name: 'Carol' },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
const GET_ROW_ID = (row: { id: number }) => row.id;
|
|
12
|
-
const GROUPING_MODEL = ['dept'];
|
|
13
|
-
const AGGREGATION_MODEL = {};
|
|
14
|
-
|
|
15
|
-
const BASE_PARAMS = {
|
|
16
|
-
rows: ROWS,
|
|
17
|
-
getRowId: GET_ROW_ID,
|
|
18
|
-
rowGroupingModel: GROUPING_MODEL,
|
|
19
|
-
aggregationModel: AGGREGATION_MODEL,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
describe('useRowGrouping — rowMetaMap', () => {
|
|
23
|
-
it('includes treeDepth for group rows', () => {
|
|
24
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
25
|
-
const map = result.current.rowMetaMap;
|
|
26
|
-
// Group rows are at depth 0 (top-level)
|
|
27
|
-
let foundGroupAtDepth0 = false;
|
|
28
|
-
map.forEach((meta, _id) => {
|
|
29
|
-
if (meta.isGroupRow) {
|
|
30
|
-
expect(meta.treeDepth).toBe(0);
|
|
31
|
-
foundGroupAtDepth0 = true;
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
expect(foundGroupAtDepth0).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('includes treeDepth for leaf rows', () => {
|
|
38
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
39
|
-
const map = result.current.rowMetaMap;
|
|
40
|
-
// Leaf rows (original rows) are at depth 1 (under the group)
|
|
41
|
-
let foundLeafAtDepth1 = false;
|
|
42
|
-
map.forEach((meta, _id) => {
|
|
43
|
-
if (!meta.isGroupRow) {
|
|
44
|
-
expect(meta.treeDepth).toBe(1);
|
|
45
|
-
foundLeafAtDepth1 = true;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
expect(foundLeafAtDepth1).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('sets isGroupRow true for group rows and false for leaves', () => {
|
|
52
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
53
|
-
const map = result.current.rowMetaMap;
|
|
54
|
-
// Leaf rows have numeric ids (1, 2, 3)
|
|
55
|
-
expect(map.get(1)?.isGroupRow).toBe(false);
|
|
56
|
-
expect(map.get(2)?.isGroupRow).toBe(false);
|
|
57
|
-
expect(map.get(3)?.isGroupRow).toBe(false);
|
|
58
|
-
// Group rows use auto-generated string ids
|
|
59
|
-
let groupCount = 0;
|
|
60
|
-
map.forEach((meta) => {
|
|
61
|
-
if (meta.isGroupRow) groupCount++;
|
|
62
|
-
});
|
|
63
|
-
// Two groups: Engineering and HR
|
|
64
|
-
expect(groupCount).toBe(2);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('includes all rows (group + leaf) in the map', () => {
|
|
68
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
69
|
-
const map = result.current.rowMetaMap;
|
|
70
|
-
// 3 leaf rows + 2 group rows = 5 entries
|
|
71
|
-
expect(map.size).toBe(5);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('has hasChildren true for group rows and false for leaf rows', () => {
|
|
75
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
76
|
-
const map = result.current.rowMetaMap;
|
|
77
|
-
expect(map.get(1)?.hasChildren).toBe(false);
|
|
78
|
-
map.forEach((meta) => {
|
|
79
|
-
if (meta.isGroupRow) {
|
|
80
|
-
expect(meta.hasChildren).toBe(true);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('populates groupLabel from groupingValueFormatter when provided', () => {
|
|
86
|
-
const formatter = ({ field, value }: { field: string; value: unknown }) =>
|
|
87
|
-
`${String(field).toUpperCase()}: ${String(value)}`;
|
|
88
|
-
const columns = [{ field: 'dept', groupingValueFormatter: formatter }];
|
|
89
|
-
const { result } = renderHook(() =>
|
|
90
|
-
useRowGrouping({ ...BASE_PARAMS, columns })
|
|
91
|
-
);
|
|
92
|
-
const map = result.current.rowMetaMap;
|
|
93
|
-
let foundFormatted = false;
|
|
94
|
-
map.forEach((meta) => {
|
|
95
|
-
if (meta.isGroupRow) {
|
|
96
|
-
expect(meta.groupLabel).toMatch(/^DEPT:/);
|
|
97
|
-
foundFormatted = true;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
expect(foundFormatted).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('uses default field: value label when groupingValueFormatter is absent', () => {
|
|
104
|
-
const { result } = renderHook(() => useRowGrouping(BASE_PARAMS));
|
|
105
|
-
const map = result.current.rowMetaMap;
|
|
106
|
-
map.forEach((meta) => {
|
|
107
|
-
if (meta.isGroupRow) {
|
|
108
|
-
expect(meta.groupLabel).toMatch(/^dept:/);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('useRowGrouping — groupable: false', () => {
|
|
115
|
-
it('skips a column with groupable:false — no group rows created for that field', () => {
|
|
116
|
-
const columns = [{ field: 'dept', groupable: false as const }];
|
|
117
|
-
const { result } = renderHook(() =>
|
|
118
|
-
useRowGrouping({ ...BASE_PARAMS, columns })
|
|
119
|
-
);
|
|
120
|
-
const map = result.current.rowMetaMap;
|
|
121
|
-
// With groupable:false on 'dept', no group rows should exist
|
|
122
|
-
let groupCount = 0;
|
|
123
|
-
map.forEach((meta) => { if (meta.isGroupRow) groupCount++; });
|
|
124
|
-
expect(groupCount).toBe(0);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('still groups when groupable is true (explicit)', () => {
|
|
128
|
-
const columns = [{ field: 'dept', groupable: true as const }];
|
|
129
|
-
const { result } = renderHook(() =>
|
|
130
|
-
useRowGrouping({ ...BASE_PARAMS, columns })
|
|
131
|
-
);
|
|
132
|
-
const map = result.current.rowMetaMap;
|
|
133
|
-
let groupCount = 0;
|
|
134
|
-
map.forEach((meta) => { if (meta.isGroupRow) groupCount++; });
|
|
135
|
-
expect(groupCount).toBe(2);
|
|
136
|
-
});
|
|
137
|
-
});
|