@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,298 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderHook, act } 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 — multi-field rowGroupingModel', () => {
|
|
115
|
-
const MULTI_ROWS = [
|
|
116
|
-
{ id: 1, dept: 'Engineering', country: 'US', name: 'Alice' },
|
|
117
|
-
{ id: 2, dept: 'Engineering', country: 'US', name: 'Bob' },
|
|
118
|
-
{ id: 3, dept: 'Engineering', country: 'UK', name: 'Carol' },
|
|
119
|
-
{ id: 4, dept: 'HR', country: 'US', name: 'Dave' },
|
|
120
|
-
];
|
|
121
|
-
const MULTI_PARAMS = {
|
|
122
|
-
rows: MULTI_ROWS,
|
|
123
|
-
getRowId: GET_ROW_ID,
|
|
124
|
-
rowGroupingModel: ['dept', 'country'],
|
|
125
|
-
aggregationModel: AGGREGATION_MODEL,
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
it('nests one group level per field, in rowGroupingModel order', () => {
|
|
129
|
-
const { result } = renderHook(() => useRowGrouping(MULTI_PARAMS));
|
|
130
|
-
const map = result.current.rowMetaMap;
|
|
131
|
-
|
|
132
|
-
const depts = new Set<unknown>();
|
|
133
|
-
const countries = new Set<unknown>();
|
|
134
|
-
map.forEach((meta) => {
|
|
135
|
-
if (!meta.isGroupRow) return;
|
|
136
|
-
if (meta.treeDepth === 0) {
|
|
137
|
-
expect(meta.groupingField).toBe('dept');
|
|
138
|
-
depts.add(meta.groupingValue);
|
|
139
|
-
} else if (meta.treeDepth === 1) {
|
|
140
|
-
expect(meta.groupingField).toBe('country');
|
|
141
|
-
countries.add(meta.groupingValue);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
// dept groups: Engineering, HR
|
|
145
|
-
expect(depts).toEqual(new Set(['Engineering', 'HR']));
|
|
146
|
-
// country groups: US (under Engineering), UK (under Engineering), US (under HR)
|
|
147
|
-
expect(countries).toEqual(new Set(['US', 'UK']));
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('places leaf rows at depth === rowGroupingModel.length', () => {
|
|
151
|
-
const { result } = renderHook(() => useRowGrouping(MULTI_PARAMS));
|
|
152
|
-
const map = result.current.rowMetaMap;
|
|
153
|
-
|
|
154
|
-
[1, 2, 3, 4].forEach((id) => {
|
|
155
|
-
const meta = map.get(id);
|
|
156
|
-
expect(meta?.isGroupRow).toBe(false);
|
|
157
|
-
expect(meta?.treeDepth).toBe(2);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('produces one depth-0 group per dept, one depth-1 group per (dept, country) pair', () => {
|
|
162
|
-
const { result } = renderHook(() => useRowGrouping(MULTI_PARAMS));
|
|
163
|
-
const map = result.current.rowMetaMap;
|
|
164
|
-
|
|
165
|
-
let depth0Count = 0;
|
|
166
|
-
let depth1Count = 0;
|
|
167
|
-
map.forEach((meta) => {
|
|
168
|
-
if (!meta.isGroupRow) return;
|
|
169
|
-
if (meta.treeDepth === 0) depth0Count++;
|
|
170
|
-
if (meta.treeDepth === 1) depth1Count++;
|
|
171
|
-
});
|
|
172
|
-
// depth 0: Engineering, HR
|
|
173
|
-
expect(depth0Count).toBe(2);
|
|
174
|
-
// depth 1: Engineering/US, Engineering/UK, HR/US
|
|
175
|
-
expect(depth1Count).toBe(3);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('reveals the next level down only after both ancestor groups are expanded', () => {
|
|
179
|
-
const { result } = renderHook(() => useRowGrouping(MULTI_PARAMS));
|
|
180
|
-
|
|
181
|
-
// Fully collapsed: only depth-0 groups are visible.
|
|
182
|
-
let visible = result.current.getVisibleRows();
|
|
183
|
-
expect(visible).not.toBeNull();
|
|
184
|
-
expect(visible!.length).toBe(2);
|
|
185
|
-
|
|
186
|
-
const deptGroupId = [...result.current.treeNodes.entries()]
|
|
187
|
-
.find(([, node]) => node.depth === 0 && node.groupingValue === 'Engineering')![0];
|
|
188
|
-
|
|
189
|
-
act(() => { result.current.toggleExpansion(deptGroupId); });
|
|
190
|
-
visible = result.current.getVisibleRows();
|
|
191
|
-
// Engineering expanded reveals its 2 country subgroups; HR still collapsed (1 group).
|
|
192
|
-
expect(visible!.length).toBe(2 + 2);
|
|
193
|
-
|
|
194
|
-
// 'US' also exists as a country subgroup under HR — select by parentId to
|
|
195
|
-
// disambiguate and target Engineering's own US subgroup.
|
|
196
|
-
const countryGroupId = [...result.current.treeNodes.entries()]
|
|
197
|
-
.find(([, node]) => node.depth === 1 && node.groupingValue === 'US' && node.parentId === deptGroupId)![0];
|
|
198
|
-
|
|
199
|
-
act(() => { result.current.toggleExpansion(countryGroupId); });
|
|
200
|
-
visible = result.current.getVisibleRows();
|
|
201
|
-
// Expanding Engineering/US additionally reveals its 2 leaf rows (Alice, Bob).
|
|
202
|
-
expect(visible!.length).toBe(2 + 2 + 2);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('useRowGrouping — groupable: false', () => {
|
|
207
|
-
it('skips a column with groupable:false — no group rows created for that field', () => {
|
|
208
|
-
const columns = [{ field: 'dept', groupable: false as const }];
|
|
209
|
-
const { result } = renderHook(() =>
|
|
210
|
-
useRowGrouping({ ...BASE_PARAMS, columns })
|
|
211
|
-
);
|
|
212
|
-
const map = result.current.rowMetaMap;
|
|
213
|
-
// With groupable:false on 'dept', no group rows should exist
|
|
214
|
-
let groupCount = 0;
|
|
215
|
-
map.forEach((meta) => { if (meta.isGroupRow) groupCount++; });
|
|
216
|
-
expect(groupCount).toBe(0);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('still groups when groupable is true (explicit)', () => {
|
|
220
|
-
const columns = [{ field: 'dept', groupable: true as const }];
|
|
221
|
-
const { result } = renderHook(() =>
|
|
222
|
-
useRowGrouping({ ...BASE_PARAMS, columns })
|
|
223
|
-
);
|
|
224
|
-
const map = result.current.rowMetaMap;
|
|
225
|
-
let groupCount = 0;
|
|
226
|
-
map.forEach((meta) => { if (meta.isGroupRow) groupCount++; });
|
|
227
|
-
expect(groupCount).toBe(2);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe('useRowGrouping — group aggregation matches useAggregation semantics', () => {
|
|
232
|
-
const NULL_ROWS = [
|
|
233
|
-
{ id: 1, g: 'A', v: 10 as number | null },
|
|
234
|
-
{ id: 2, g: 'A', v: null as number | null },
|
|
235
|
-
{ id: 3, g: 'A', v: 20 as number | null },
|
|
236
|
-
];
|
|
237
|
-
const MODEL = ['g'];
|
|
238
|
-
const aggFor = (fn: string, columns?: { field: string; availableAggregationFunctions?: string[] }[]) => {
|
|
239
|
-
const aggregationModel = { v: fn };
|
|
240
|
-
const { result } = renderHook(() => useRowGrouping({ rows: NULL_ROWS, getRowId: GET_ROW_ID, rowGroupingModel: MODEL, aggregationModel, columns }));
|
|
241
|
-
return [...result.current.treeNodes.values()].find(n => n.depth === 0)!.aggregatedValues!;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
it('ignores nulls for min / avg / count instead of treating them as 0', () => {
|
|
245
|
-
expect(aggFor('min').v).toBe(10);
|
|
246
|
-
expect(aggFor('avg').v).toBe(15);
|
|
247
|
-
expect(aggFor('count').v).toBe(2);
|
|
248
|
-
expect(aggFor('sum').v).toBe(30);
|
|
249
|
-
expect(aggFor('max').v).toBe(20);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('supports the unique aggregation', () => {
|
|
253
|
-
expect(aggFor('unique').v).toBe(2);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('honors availableAggregationFunctions', () => {
|
|
257
|
-
expect(aggFor('avg', [{ field: 'v', availableAggregationFunctions: ['sum'] }])).not.toHaveProperty('v');
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('useRowGrouping — expansion state', () => {
|
|
262
|
-
it('preserves user expansion when rows are replaced with edited copies', () => {
|
|
263
|
-
const { result, rerender } = renderHook(({ rows }) => useRowGrouping({ ...BASE_PARAMS, rows }), { initialProps: { rows: ROWS } });
|
|
264
|
-
const eng = [...result.current.treeNodes.values()].find(n => n.groupingValue === 'Engineering')!.id;
|
|
265
|
-
act(() => { result.current.toggleExpansion(eng); });
|
|
266
|
-
rerender({ rows: ROWS.map(r => (r.id === 1 ? { ...r, name: 'Alicia' } : r)) });
|
|
267
|
-
expect(result.current.isGroupExpanded(eng)).toBe(true);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('preserves user expansion when an equal rowGroupingModel is passed as a new array', () => {
|
|
271
|
-
const { result, rerender } = renderHook(({ model }) => useRowGrouping({ ...BASE_PARAMS, rowGroupingModel: model }), { initialProps: { model: ['dept'] } });
|
|
272
|
-
const eng = [...result.current.treeNodes.values()].find(n => n.groupingValue === 'Engineering')!.id;
|
|
273
|
-
act(() => { result.current.toggleExpansion(eng); });
|
|
274
|
-
rerender({ model: ['dept'] });
|
|
275
|
-
expect(result.current.isGroupExpanded(eng)).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('discards user overrides when defaultGroupingExpansionDepth changes', () => {
|
|
279
|
-
const { result, rerender } = renderHook(({ depth }) => useRowGrouping({ ...BASE_PARAMS, defaultGroupingExpansionDepth: depth }), { initialProps: { depth: -1 } });
|
|
280
|
-
const hr = [...result.current.treeNodes.values()].find(n => n.groupingValue === 'HR')!.id;
|
|
281
|
-
expect(result.current.isGroupExpanded(hr)).toBe(true);
|
|
282
|
-
act(() => { result.current.toggleExpansion(hr); });
|
|
283
|
-
expect(result.current.isGroupExpanded(hr)).toBe(false);
|
|
284
|
-
// depth 1 also expands depth-0 groups by default; the collapse override must not survive.
|
|
285
|
-
rerender({ depth: 1 });
|
|
286
|
-
expect(result.current.isGroupExpanded(hr)).toBe(true);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('does not reorder the memoized tree when getVisibleRows sorts', () => {
|
|
290
|
-
const { result } = renderHook(() => useRowGrouping({ ...BASE_PARAMS, sortModel: SORT_DESC, defaultGroupingExpansionDepth: -1 }));
|
|
291
|
-
const eng = [...result.current.treeNodes.values()].find(n => n.groupingValue === 'Engineering')!;
|
|
292
|
-
const before = [...eng.children!];
|
|
293
|
-
result.current.getVisibleRows();
|
|
294
|
-
expect(eng.children).toEqual(before);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const SORT_DESC = [{ field: 'name', sort: 'desc' as const }];
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { renderHook, act } from '@testing-library/react';
|
|
3
|
-
import { useTreeData } from './useTreeData';
|
|
4
|
-
import type { GridRowModel, GridRowId } from '../types';
|
|
5
|
-
|
|
6
|
-
type Row = GridRowModel & { id: number; path: string[]; name: string };
|
|
7
|
-
|
|
8
|
-
const ROWS: Row[] = [
|
|
9
|
-
{ id: 1, path: ['CEO'], name: 'Ada' },
|
|
10
|
-
{ id: 2, path: ['CEO', 'CTO'], name: 'Bo' },
|
|
11
|
-
{ id: 3, path: ['CEO', 'CTO', 'Dev'], name: 'Cy' },
|
|
12
|
-
{ id: 4, path: ['CEO', 'CFO'], name: 'Di' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const getRowId = (r: Row) => r.id;
|
|
16
|
-
const getTreeDataPath = (r: Row) => r.path;
|
|
17
|
-
|
|
18
|
-
const visibleIds = (rows: GridRowModel[] | null) => (rows ?? []).map(r => r.id as GridRowId);
|
|
19
|
-
|
|
20
|
-
describe('useTreeData — expansion state', () => {
|
|
21
|
-
it('expands to defaultGroupingExpansionDepth on the first render (no effect flash)', () => {
|
|
22
|
-
const { result } = renderHook(() =>
|
|
23
|
-
useTreeData({ rows: ROWS, getRowId, getTreeDataPath, treeData: true, defaultGroupingExpansionDepth: 1 })
|
|
24
|
-
);
|
|
25
|
-
expect(result.current.isGroupExpanded(1)).toBe(true);
|
|
26
|
-
expect(result.current.isGroupExpanded(2)).toBe(false);
|
|
27
|
-
expect(visibleIds(result.current.getVisibleRows())).toEqual([1, 2, 4]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('keeps a user collapse when rows are replaced with edited copies', () => {
|
|
31
|
-
const { result, rerender } = renderHook(
|
|
32
|
-
({ rows }) => useTreeData({ rows, getRowId, getTreeDataPath, treeData: true, defaultGroupingExpansionDepth: -1 }),
|
|
33
|
-
{ initialProps: { rows: ROWS } }
|
|
34
|
-
);
|
|
35
|
-
act(() => { result.current.toggleExpansion(2); });
|
|
36
|
-
expect(result.current.isGroupExpanded(2)).toBe(false);
|
|
37
|
-
|
|
38
|
-
rerender({ rows: ROWS.map(r => (r.id === 4 ? { ...r, name: 'Dee' } : r)) });
|
|
39
|
-
expect(result.current.isGroupExpanded(2)).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('keeps a lazily expanded node open after its children arrive', () => {
|
|
43
|
-
const lazyRows: Row[] = [
|
|
44
|
-
{ id: 1, path: ['CEO'], name: 'Ada' },
|
|
45
|
-
{ id: 2, path: ['CEO', 'CTO'], name: 'Bo', serverChildrenCount: 1 },
|
|
46
|
-
];
|
|
47
|
-
const { result, rerender } = renderHook(
|
|
48
|
-
({ rows }) => useTreeData({ rows, getRowId, getTreeDataPath, treeData: true, defaultGroupingExpansionDepth: 1 }),
|
|
49
|
-
{ initialProps: { rows: lazyRows } }
|
|
50
|
-
);
|
|
51
|
-
act(() => { result.current.toggleExpansion(2); });
|
|
52
|
-
expect(result.current.isGroupExpanded(2)).toBe(true);
|
|
53
|
-
|
|
54
|
-
rerender({ rows: [...lazyRows, { id: 3, path: ['CEO', 'CTO', 'Dev'], name: 'Cy' }] });
|
|
55
|
-
expect(result.current.isGroupExpanded(2)).toBe(true);
|
|
56
|
-
expect(visibleIds(result.current.getVisibleRows())).toContain(3);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('calls onRowExpansionChange exactly once per expand and never on collapse', () => {
|
|
60
|
-
const onRowExpansionChange = vi.fn();
|
|
61
|
-
const { result } = renderHook(() =>
|
|
62
|
-
useTreeData({ rows: ROWS, getRowId, getTreeDataPath, treeData: true, onRowExpansionChange })
|
|
63
|
-
);
|
|
64
|
-
act(() => { result.current.toggleExpansion(1); });
|
|
65
|
-
expect(onRowExpansionChange).toHaveBeenCalledTimes(1);
|
|
66
|
-
expect(onRowExpansionChange).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }));
|
|
67
|
-
|
|
68
|
-
act(() => { result.current.toggleExpansion(1); });
|
|
69
|
-
expect(onRowExpansionChange).toHaveBeenCalledTimes(1);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('discards user overrides when defaultGroupingExpansionDepth changes', () => {
|
|
73
|
-
const { result, rerender } = renderHook(
|
|
74
|
-
({ depth }) => useTreeData({ rows: ROWS, getRowId, getTreeDataPath, treeData: true, defaultGroupingExpansionDepth: depth }),
|
|
75
|
-
{ initialProps: { depth: -1 } }
|
|
76
|
-
);
|
|
77
|
-
act(() => { result.current.toggleExpansion(1); });
|
|
78
|
-
expect(result.current.isGroupExpanded(1)).toBe(false);
|
|
79
|
-
|
|
80
|
-
rerender({ depth: 1 });
|
|
81
|
-
expect(result.current.isGroupExpanded(1)).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { AGGREGATION_FUNCTIONS } from './index';
|
|
3
|
-
|
|
4
|
-
describe('AGGREGATION_FUNCTIONS', () => {
|
|
5
|
-
it('computes min / max over more values than a spread argument list allows', () => {
|
|
6
|
-
const values = Array.from({ length: 300_000 }, (_, i) => i);
|
|
7
|
-
expect(AGGREGATION_FUNCTIONS.min(values)).toBe(0);
|
|
8
|
-
expect(AGGREGATION_FUNCTIONS.max(values)).toBe(299_999);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('returns null for min / max / avg when there are no numeric values', () => {
|
|
12
|
-
expect(AGGREGATION_FUNCTIONS.min([null, undefined])).toBeNull();
|
|
13
|
-
expect(AGGREGATION_FUNCTIONS.max([])).toBeNull();
|
|
14
|
-
expect(AGGREGATION_FUNCTIONS.avg(['x'])).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import ExcelJS from 'exceljs';
|
|
3
|
-
import { exportToExcelAdvanced } from './exportToExcelAdvanced';
|
|
4
|
-
import type { GridColDef, GridGroupedExportRow, GridRowModel } from '../../types';
|
|
5
|
-
|
|
6
|
-
type Row = GridRowModel & { id: number; dept: string; salary: number };
|
|
7
|
-
|
|
8
|
-
const columns: GridColDef<Row>[] = [
|
|
9
|
-
{ field: 'dept', headerName: 'Department' },
|
|
10
|
-
{ field: 'salary', headerName: 'Salary', type: 'number' },
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
const flatRows: Row[] = [
|
|
14
|
-
{ id: 1, dept: 'Engineering', salary: 90000 },
|
|
15
|
-
{ id: 2, dept: 'Engineering', salary: 85000 },
|
|
16
|
-
{ id: 3, dept: 'Marketing', salary: 70000 },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const groupedRows: GridGroupedExportRow[] = [
|
|
20
|
-
{ type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Engineering' },
|
|
21
|
-
{ type: 'leaf', depth: 1, row: flatRows[0] },
|
|
22
|
-
{ type: 'leaf', depth: 1, row: flatRows[1] },
|
|
23
|
-
{ type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Engineering', aggregatedValues: { salary: 175000 } },
|
|
24
|
-
{ type: 'group-header', depth: 0, groupField: 'dept', groupValue: 'Marketing' },
|
|
25
|
-
{ type: 'leaf', depth: 1, row: flatRows[2] },
|
|
26
|
-
{ type: 'group-subtotal', depth: 0, groupField: 'dept', groupValue: 'Marketing', aggregatedValues: { salary: 70000 } },
|
|
27
|
-
{ type: 'grand-total', depth: 0, aggregatedValues: { salary: 245000 } },
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
let capturedBlob: Blob | null = null;
|
|
31
|
-
const origCreateObjectURL = URL.createObjectURL;
|
|
32
|
-
const origRevokeObjectURL = URL.revokeObjectURL;
|
|
33
|
-
const origClick = HTMLAnchorElement.prototype.click;
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
capturedBlob = null;
|
|
37
|
-
URL.createObjectURL = (blob: Blob) => { capturedBlob = blob; return 'blob:mock'; };
|
|
38
|
-
URL.revokeObjectURL = () => {};
|
|
39
|
-
HTMLAnchorElement.prototype.click = () => {};
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
URL.createObjectURL = origCreateObjectURL;
|
|
44
|
-
URL.revokeObjectURL = origRevokeObjectURL;
|
|
45
|
-
HTMLAnchorElement.prototype.click = origClick;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
async function readSheet(): Promise<ExcelJS.Worksheet> {
|
|
49
|
-
const wb = new ExcelJS.Workbook();
|
|
50
|
-
await wb.xlsx.load(await capturedBlob!.arrayBuffer());
|
|
51
|
-
return wb.worksheets[0];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const rowValues = (ws: ExcelJS.Worksheet) =>
|
|
55
|
-
ws.getSheetValues().slice(1).map(r => (Array.isArray(r) ? r.slice(1) : []));
|
|
56
|
-
|
|
57
|
-
describe('exportToExcelAdvanced — flat', () => {
|
|
58
|
-
it('writes a header row and one row per record when groupedRows is absent', async () => {
|
|
59
|
-
await exportToExcelAdvanced(flatRows, columns);
|
|
60
|
-
const ws = await readSheet();
|
|
61
|
-
expect(rowValues(ws)).toEqual([
|
|
62
|
-
['Department', 'Salary'],
|
|
63
|
-
['Engineering', 90000],
|
|
64
|
-
['Engineering', 85000],
|
|
65
|
-
['Marketing', 70000],
|
|
66
|
-
]);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('exportToExcelAdvanced — grouped', () => {
|
|
71
|
-
it('writes group headers, leaves, subtotals and a single grand total in order', async () => {
|
|
72
|
-
await exportToExcelAdvanced(flatRows, columns, {
|
|
73
|
-
groupedRows,
|
|
74
|
-
aggregationModel: { salary: 'sum' },
|
|
75
|
-
aggregationResult: { salary: 245000 },
|
|
76
|
-
sheets: [{ name: 'Data', includeSummary: true }],
|
|
77
|
-
});
|
|
78
|
-
const ws = await readSheet();
|
|
79
|
-
expect(rowValues(ws)).toEqual([
|
|
80
|
-
['Department', 'Salary'],
|
|
81
|
-
['Department: Engineering'],
|
|
82
|
-
['Engineering', 90000],
|
|
83
|
-
['Engineering', 85000],
|
|
84
|
-
['Subtotal', 175000],
|
|
85
|
-
['Department: Marketing'],
|
|
86
|
-
['Marketing', 70000],
|
|
87
|
-
['Subtotal', 70000],
|
|
88
|
-
['Grand Total', 245000],
|
|
89
|
-
]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('outlines leaf rows one level below their group header', async () => {
|
|
93
|
-
await exportToExcelAdvanced(flatRows, columns, { groupedRows });
|
|
94
|
-
const ws = await readSheet();
|
|
95
|
-
expect(ws.getRow(2).outlineLevel ?? 0).toBe(0);
|
|
96
|
-
expect(ws.getRow(3).outlineLevel).toBe(1);
|
|
97
|
-
expect(ws.getRow(4).outlineLevel).toBe(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('uses groupingValueFormatter for group-header labels', async () => {
|
|
101
|
-
const cols: GridColDef<Row>[] = [
|
|
102
|
-
{ ...columns[0], groupingValueFormatter: ({ value }) => `Dept ${String(value)}` },
|
|
103
|
-
columns[1],
|
|
104
|
-
];
|
|
105
|
-
await exportToExcelAdvanced(flatRows, cols, { groupedRows });
|
|
106
|
-
const ws = await readSheet();
|
|
107
|
-
expect(ws.getRow(2).getCell(1).value).toBe('Dept Engineering');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import type { GridColDef, GridRowModel } from '../../types';
|
|
3
|
-
|
|
4
|
-
// --- jsPDF mocks ---
|
|
5
|
-
const mockAutoTable = vi.fn();
|
|
6
|
-
const mockSave = vi.fn();
|
|
7
|
-
const mockSetFontSize = vi.fn();
|
|
8
|
-
const mockSetFont = vi.fn();
|
|
9
|
-
const mockSetTextColor = vi.fn();
|
|
10
|
-
const mockText = vi.fn();
|
|
11
|
-
const mockLine = vi.fn();
|
|
12
|
-
const mockSetDrawColor = vi.fn();
|
|
13
|
-
const mockSetLineWidth = vi.fn();
|
|
14
|
-
const mockAddImage = vi.fn();
|
|
15
|
-
const mockSetPage = vi.fn();
|
|
16
|
-
|
|
17
|
-
class MockJsPDF {
|
|
18
|
-
save = mockSave;
|
|
19
|
-
setFontSize = mockSetFontSize;
|
|
20
|
-
setFont = mockSetFont;
|
|
21
|
-
setTextColor = mockSetTextColor;
|
|
22
|
-
text = mockText;
|
|
23
|
-
line = mockLine;
|
|
24
|
-
setDrawColor = mockSetDrawColor;
|
|
25
|
-
setLineWidth = mockSetLineWidth;
|
|
26
|
-
addImage = mockAddImage;
|
|
27
|
-
setPage = mockSetPage;
|
|
28
|
-
lastAutoTable = { finalY: 50 };
|
|
29
|
-
internal = {
|
|
30
|
-
pageSize: { getWidth: () => 297, getHeight: () => 210 },
|
|
31
|
-
getNumberOfPages: () => 1,
|
|
32
|
-
};
|
|
33
|
-
getNumberOfPages = () => 1;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
vi.mock('jspdf', () => ({ default: MockJsPDF }));
|
|
37
|
-
vi.mock('jspdf-autotable', () => ({ default: mockAutoTable }));
|
|
38
|
-
|
|
39
|
-
const sampleColumns: GridColDef[] = [
|
|
40
|
-
{ field: 'name', headerName: 'Name', width: 150 },
|
|
41
|
-
{ field: 'salary', headerName: 'Salary', width: 120, valueFormatter: ({ value }) => `$${value}` },
|
|
42
|
-
{ field: 'hidden', headerName: 'Hidden', width: 100, exportable: false },
|
|
43
|
-
{ field: '__check__', headerName: '', width: 50 },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const sampleRows: GridRowModel[] = [
|
|
47
|
-
{ id: 1, name: 'Alice', salary: 90000, hidden: 'secret' },
|
|
48
|
-
{ id: 2, name: 'Bob', salary: 80000, hidden: 'secret' },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
describe('exportToPdf', () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
mockAutoTable.mockClear();
|
|
54
|
-
mockSave.mockClear();
|
|
55
|
-
mockText.mockClear();
|
|
56
|
-
mockAddImage.mockClear();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('calls doc.save with the correct filename', async () => {
|
|
60
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
61
|
-
await exportToPdf(sampleRows, sampleColumns, { fileName: 'my-report' });
|
|
62
|
-
expect(mockSave).toHaveBeenCalledWith('my-report.pdf');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('uses default filename "export.pdf" when not provided', async () => {
|
|
66
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
67
|
-
await exportToPdf(sampleRows, sampleColumns);
|
|
68
|
-
expect(mockSave).toHaveBeenCalledWith('export.pdf');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('excludes columns with exportable: false and system columns', async () => {
|
|
72
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
73
|
-
await exportToPdf(sampleRows, sampleColumns);
|
|
74
|
-
const call = mockAutoTable.mock.calls[0][1];
|
|
75
|
-
// head should only have Name and Salary — not Hidden or __check__
|
|
76
|
-
expect(call.head[0]).toEqual(['Name', 'Salary']);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('applies valueFormatter to cell values', async () => {
|
|
80
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
81
|
-
await exportToPdf(sampleRows, sampleColumns);
|
|
82
|
-
const call = mockAutoTable.mock.calls[0][1];
|
|
83
|
-
// salary column should be formatted
|
|
84
|
-
expect(call.body[0][1]).toBe('$90000');
|
|
85
|
-
expect(call.body[1][1]).toBe('$80000');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('filters to selectedRows when provided', async () => {
|
|
89
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
90
|
-
await exportToPdf(sampleRows, sampleColumns, { selectedRows: [1] });
|
|
91
|
-
const call = mockAutoTable.mock.calls[0][1];
|
|
92
|
-
expect(call.body).toHaveLength(1);
|
|
93
|
-
expect(call.body[0][0]).toBe('Alice');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('appends aggregation footer when aggregationResult is provided', async () => {
|
|
97
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
98
|
-
await exportToPdf(sampleRows, sampleColumns, {
|
|
99
|
-
aggregationResult: { salary: 170000 },
|
|
100
|
-
aggregationModel: { salary: 'sum' },
|
|
101
|
-
});
|
|
102
|
-
const call = mockAutoTable.mock.calls[0][1];
|
|
103
|
-
expect(call.foot).toBeDefined();
|
|
104
|
-
expect(call.foot[0][1]).toBe('$170000');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('renders title text when title option is set', async () => {
|
|
108
|
-
const { exportToPdf } = await import('./exportToPdf');
|
|
109
|
-
await exportToPdf(sampleRows, sampleColumns, { title: 'Sales Report' });
|
|
110
|
-
// doc.text should have been called with the title
|
|
111
|
-
const textCalls = mockText.mock.calls.map((c: unknown[]) => c[0]);
|
|
112
|
-
expect(textCalls.some((t: unknown) => t === 'Sales Report')).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('throws descriptive error when jspdf is not installed', async () => {
|
|
116
|
-
// This test uses a different mock setup — skip in unit suite,
|
|
117
|
-
// documented as an integration concern
|
|
118
|
-
expect(true).toBe(true);
|
|
119
|
-
});
|
|
120
|
-
});
|