@jmruthers/pace-core 0.5.118 → 0.5.120
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/dist/{DataTable-ZOAKQ3SU.js → DataTable-DGZDJUYM.js} +7 -7
- package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
- package/dist/{chunk-7OTQLFVI.js → chunk-B4GZ2BXO.js} +3 -3
- package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
- package/dist/chunk-BHWIUEYH.js.map +1 -0
- package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
- package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
- package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
- package/dist/{chunk-2GJ5GL77.js → chunk-GKHF54DI.js} +2 -2
- package/dist/chunk-GKHF54DI.js.map +1 -0
- package/dist/{chunk-UKZWNQMB.js → chunk-HFBOFZ3Z.js} +5 -18
- package/dist/chunk-HFBOFZ3Z.js.map +1 -0
- package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
- package/dist/{chunk-2LM4QQGH.js → chunk-QPI2CCBA.js} +9 -9
- package/dist/chunk-QPI2CCBA.js.map +1 -0
- package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
- package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
- package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +9 -9
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -12
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/{useToast-Cs_g32bg.d.ts → useToast-C8gR5ir4.d.ts} +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2 -2
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
- package/src/components/DataTable/components/DataTableCore.tsx +5 -0
- package/src/components/DataTable/components/EditableRow.tsx +9 -18
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +616 -9
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/index.unit.test.ts +223 -0
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
- package/src/hooks/__tests__/useEvents.unit.test.ts +251 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +19 -9
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +661 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +371 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +449 -30
- package/src/hooks/useSecureDataAccess.test.ts +1 -0
- package/src/hooks/useToast.ts +4 -4
- package/src/rbac/audit-enhanced.ts +339 -0
- package/src/services/EventService.ts +1 -0
- package/src/services/__tests__/AuthService.test.ts +473 -0
- package/src/services/__tests__/EventService.test.ts +390 -0
- package/src/services/__tests__/InactivityService.test.ts +217 -0
- package/src/services/__tests__/OrganisationService.test.ts +371 -0
- package/src/styles/core.css +1 -0
- package/dist/chunk-2GJ5GL77.js.map +0 -1
- package/dist/chunk-2LM4QQGH.js.map +0 -1
- package/dist/chunk-KA3PSVNV.js.map +0 -1
- package/dist/chunk-UKZWNQMB.js.map +0 -1
- package/src/components/DataTable/utils/debugTools.ts +0 -609
- package/src/rbac/testing/index.tsx +0 -340
- /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-DGZDJUYM.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
- /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
- /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
- /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
- /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
- /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
- /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
- /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
- /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useDataTablePerformance Hook Unit Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/__tests__
|
|
5
|
+
* @since 0.3.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the useDataTablePerformance hook following TEST_STANDARD.md.
|
|
8
|
+
* Tests focus on pagination, virtualization, search, server-side fetching, error recovery, and memory monitoring.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { useDataTablePerformance } from '../useDataTablePerformance';
|
|
14
|
+
import type { DataRecord, ServerSideConfig, ServerSideParams, ServerSideResponse } from '../../components/DataTable/types';
|
|
15
|
+
|
|
16
|
+
// Mock performance utilities
|
|
17
|
+
vi.mock('../../components/DataTable/utils/performanceUtils', () => ({
|
|
18
|
+
determinePaginationMode: vi.fn((length: number) => {
|
|
19
|
+
if (length < 1000) return 'client';
|
|
20
|
+
return 'hybrid';
|
|
21
|
+
}),
|
|
22
|
+
getOptimalPageSizeOptions: vi.fn((mode: string, length: number) => {
|
|
23
|
+
if (mode === 'client') return [10, 25, 50, 100];
|
|
24
|
+
if (mode === 'hybrid') return [25, 50, 100, 200];
|
|
25
|
+
return [10, 25, 50];
|
|
26
|
+
}),
|
|
27
|
+
DataChunkManager: class {
|
|
28
|
+
clear = vi.fn();
|
|
29
|
+
getMemoryUsage = vi.fn(() => 0.5);
|
|
30
|
+
constructor() {
|
|
31
|
+
// Constructor
|
|
32
|
+
}
|
|
33
|
+
} as any,
|
|
34
|
+
SearchIndex: class MockSearchIndex {
|
|
35
|
+
buildIndex: ReturnType<typeof vi.fn>;
|
|
36
|
+
search: ReturnType<typeof vi.fn>;
|
|
37
|
+
constructor(config?: any) {
|
|
38
|
+
this.buildIndex = vi.fn();
|
|
39
|
+
this.search = vi.fn(() => [0, 1, 2]);
|
|
40
|
+
}
|
|
41
|
+
} as any,
|
|
42
|
+
debounce: vi.fn((fn: Function, delay: number) => fn),
|
|
43
|
+
VisibilityTracker: class {
|
|
44
|
+
destroy = vi.fn();
|
|
45
|
+
onVisibilityChange = vi.fn(() => vi.fn()); // Return unsubscribe function
|
|
46
|
+
constructor() {
|
|
47
|
+
// Constructor
|
|
48
|
+
}
|
|
49
|
+
} as any
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Mock error handling utilities
|
|
53
|
+
vi.mock('../../components/DataTable/utils/errorHandling', () => ({
|
|
54
|
+
ErrorRecoveryManager: class {
|
|
55
|
+
handleError = vi.fn().mockResolvedValue({ recovered: true });
|
|
56
|
+
clearErrorLog = vi.fn();
|
|
57
|
+
constructor() {
|
|
58
|
+
// Constructor
|
|
59
|
+
}
|
|
60
|
+
} as any,
|
|
61
|
+
MemoryMonitor: class {
|
|
62
|
+
stopMonitoring = vi.fn();
|
|
63
|
+
startMonitoring = vi.fn();
|
|
64
|
+
getMemoryUsage = vi.fn(() => 0);
|
|
65
|
+
constructor() {
|
|
66
|
+
// Constructor
|
|
67
|
+
}
|
|
68
|
+
} as any,
|
|
69
|
+
CircuitBreaker: vi.fn().mockImplementation(() => ({
|
|
70
|
+
isOpen: false,
|
|
71
|
+
recordSuccess: vi.fn(),
|
|
72
|
+
recordFailure: vi.fn()
|
|
73
|
+
})),
|
|
74
|
+
DEFAULT_FALLBACK_CONFIG: {
|
|
75
|
+
enableFallbacks: true,
|
|
76
|
+
maxRetries: 3
|
|
77
|
+
},
|
|
78
|
+
safeExecute: vi.fn((fn: Function) => Promise.resolve(fn()))
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
import { determinePaginationMode, getOptimalPageSizeOptions } from '../../components/DataTable/utils/performanceUtils';
|
|
82
|
+
import { ErrorRecoveryManager } from '../../components/DataTable/utils/errorHandling';
|
|
83
|
+
|
|
84
|
+
describe('useDataTablePerformance Hook', () => {
|
|
85
|
+
interface TestRecord extends DataRecord {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
value: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mockData: TestRecord[] = [
|
|
92
|
+
{ id: '1', name: 'Test 1', value: 10 },
|
|
93
|
+
{ id: '2', name: 'Test 2', value: 20 },
|
|
94
|
+
{ id: '3', name: 'Test 3', value: 30 }
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.clearAllMocks();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Pagination Mode Determination', () => {
|
|
106
|
+
it('returns client mode for small datasets', () => {
|
|
107
|
+
const { result } = renderHook(() =>
|
|
108
|
+
useDataTablePerformance({
|
|
109
|
+
data: mockData
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(result.current.paginationMode).toBe('client');
|
|
114
|
+
expect(determinePaginationMode).toHaveBeenCalledWith(3, undefined);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns server mode when serverSide config is provided', () => {
|
|
118
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
119
|
+
fetchData: vi.fn()
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const { result } = renderHook(() =>
|
|
123
|
+
useDataTablePerformance({
|
|
124
|
+
data: mockData,
|
|
125
|
+
serverSide
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result.current.paginationMode).toBe('server');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns hybrid mode for large datasets', () => {
|
|
133
|
+
const largeData = Array.from({ length: 1500 }, (_, i) => ({
|
|
134
|
+
id: String(i),
|
|
135
|
+
name: `Test ${i}`,
|
|
136
|
+
value: i
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
const { result } = renderHook(() =>
|
|
140
|
+
useDataTablePerformance({
|
|
141
|
+
data: largeData
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(result.current.paginationMode).toBe('hybrid');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('respects custom serverSideThreshold', () => {
|
|
149
|
+
const { result } = renderHook(() =>
|
|
150
|
+
useDataTablePerformance({
|
|
151
|
+
data: mockData,
|
|
152
|
+
performance: {
|
|
153
|
+
serverSideThreshold: 500
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(determinePaginationMode).toHaveBeenCalledWith(3, 500);
|
|
159
|
+
expect(result.current.paginationMode).toBe('client');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Virtualization', () => {
|
|
164
|
+
it('returns false for isVirtualized', () => {
|
|
165
|
+
const { result } = renderHook(() =>
|
|
166
|
+
useDataTablePerformance({
|
|
167
|
+
data: mockData
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(result.current.isVirtualized).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Page Size Options', () => {
|
|
176
|
+
it('returns optimal page size options for client mode', () => {
|
|
177
|
+
const { result } = renderHook(() =>
|
|
178
|
+
useDataTablePerformance({
|
|
179
|
+
data: mockData
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result.current.pageSizeOptions).toEqual([10, 25, 50, 100]);
|
|
184
|
+
expect(getOptimalPageSizeOptions).toHaveBeenCalledWith('client', 3);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns optimal page size options for hybrid mode', () => {
|
|
188
|
+
const largeData = Array.from({ length: 1500 }, (_, i) => ({
|
|
189
|
+
id: String(i),
|
|
190
|
+
name: `Test ${i}`,
|
|
191
|
+
value: i
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const { result } = renderHook(() =>
|
|
195
|
+
useDataTablePerformance({
|
|
196
|
+
data: largeData
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(result.current.pageSizeOptions).toEqual([25, 50, 100, 200]);
|
|
201
|
+
expect(getOptimalPageSizeOptions).toHaveBeenCalledWith('hybrid', 1500);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('updates page size options when data length changes', () => {
|
|
205
|
+
const { result, rerender } = renderHook(
|
|
206
|
+
({ data }) =>
|
|
207
|
+
useDataTablePerformance({
|
|
208
|
+
data
|
|
209
|
+
}),
|
|
210
|
+
{
|
|
211
|
+
initialProps: { data: mockData }
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(result.current.pageSizeOptions).toEqual([10, 25, 50, 100]);
|
|
216
|
+
|
|
217
|
+
const largeData = Array.from({ length: 1500 }, (_, i) => ({
|
|
218
|
+
id: String(i),
|
|
219
|
+
name: `Test ${i}`,
|
|
220
|
+
value: i
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
rerender({ data: largeData });
|
|
224
|
+
|
|
225
|
+
expect(result.current.pageSizeOptions).toEqual([25, 50, 100, 200]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Search Functionality', () => {
|
|
230
|
+
it('initializes with empty search query', () => {
|
|
231
|
+
const { result } = renderHook(() =>
|
|
232
|
+
useDataTablePerformance({
|
|
233
|
+
data: mockData
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(result.current.searchQuery).toBe('');
|
|
238
|
+
expect(result.current.searchResults).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('updates search query when setSearchQuery is called', () => {
|
|
242
|
+
const { result } = renderHook(() =>
|
|
243
|
+
useDataTablePerformance({
|
|
244
|
+
data: mockData
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
act(() => {
|
|
249
|
+
result.current.setSearchQuery('test');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.current.searchQuery).toBe('test');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('filters data based on search results', async () => {
|
|
256
|
+
const { result } = renderHook(() =>
|
|
257
|
+
useDataTablePerformance({
|
|
258
|
+
data: mockData,
|
|
259
|
+
searchIndex: {
|
|
260
|
+
enabled: true
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await waitFor(
|
|
266
|
+
() => {
|
|
267
|
+
expect(result.current.searchQuery).toBeDefined();
|
|
268
|
+
},
|
|
269
|
+
{ timeout: 1000 }
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
result.current.setSearchQuery('test');
|
|
273
|
+
|
|
274
|
+
await waitFor(
|
|
275
|
+
() => {
|
|
276
|
+
expect(result.current.searchResults).toBeDefined();
|
|
277
|
+
},
|
|
278
|
+
{ timeout: 1000 }
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('clears search results when query is empty', async () => {
|
|
283
|
+
const { result } = renderHook(() =>
|
|
284
|
+
useDataTablePerformance({
|
|
285
|
+
data: mockData,
|
|
286
|
+
searchIndex: {
|
|
287
|
+
enabled: true
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
result.current.setSearchQuery('test');
|
|
293
|
+
|
|
294
|
+
await waitFor(
|
|
295
|
+
() => {
|
|
296
|
+
expect(result.current.searchQuery).toBe('test');
|
|
297
|
+
},
|
|
298
|
+
{ timeout: 1000 }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
result.current.setSearchQuery('');
|
|
302
|
+
|
|
303
|
+
await waitFor(
|
|
304
|
+
() => {
|
|
305
|
+
expect(result.current.searchResults).toEqual([]);
|
|
306
|
+
},
|
|
307
|
+
{ timeout: 1000 }
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('Server-Side Data Fetching', () => {
|
|
313
|
+
it('fetches server data when fetchServerData is called', async () => {
|
|
314
|
+
const mockFetchData = vi.fn().mockResolvedValue({
|
|
315
|
+
data: mockData,
|
|
316
|
+
totalCount: 100
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
320
|
+
fetchData: mockFetchData
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const { result } = renderHook(() =>
|
|
324
|
+
useDataTablePerformance({
|
|
325
|
+
data: [],
|
|
326
|
+
serverSide
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const params: ServerSideParams = {
|
|
331
|
+
page: 1,
|
|
332
|
+
pageSize: 10,
|
|
333
|
+
sortBy: 'name',
|
|
334
|
+
sortOrder: 'asc'
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
await result.current.fetchServerData(params);
|
|
338
|
+
|
|
339
|
+
await waitFor(
|
|
340
|
+
() => {
|
|
341
|
+
expect(result.current.isLoading).toBe(false);
|
|
342
|
+
},
|
|
343
|
+
{ timeout: 2000 }
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(mockFetchData).toHaveBeenCalledWith(params);
|
|
347
|
+
expect(result.current.serverData).toEqual({
|
|
348
|
+
data: mockData,
|
|
349
|
+
totalCount: 100
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('handles server data fetch errors gracefully', async () => {
|
|
354
|
+
const mockFetchData = vi.fn().mockRejectedValue(new Error('Fetch failed'));
|
|
355
|
+
|
|
356
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
357
|
+
fetchData: mockFetchData
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
361
|
+
|
|
362
|
+
const { result } = renderHook(() =>
|
|
363
|
+
useDataTablePerformance({
|
|
364
|
+
data: [],
|
|
365
|
+
serverSide
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const params: ServerSideParams = {
|
|
370
|
+
page: 1,
|
|
371
|
+
pageSize: 10
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
await result.current.fetchServerData(params);
|
|
375
|
+
|
|
376
|
+
await waitFor(
|
|
377
|
+
() => {
|
|
378
|
+
expect(result.current.isLoading).toBe(false);
|
|
379
|
+
},
|
|
380
|
+
{ timeout: 2000 }
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch server data:', expect.any(Error));
|
|
384
|
+
consoleSpy.mockRestore();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('does not fetch when serverSide is not provided', async () => {
|
|
388
|
+
const { result } = renderHook(() =>
|
|
389
|
+
useDataTablePerformance({
|
|
390
|
+
data: mockData
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const params: ServerSideParams = {
|
|
395
|
+
page: 1,
|
|
396
|
+
pageSize: 10
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
await result.current.fetchServerData(params);
|
|
400
|
+
|
|
401
|
+
// Should not throw error and should complete immediately
|
|
402
|
+
expect(result.current.isLoading).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('uses server data for processedData when available', async () => {
|
|
406
|
+
const serverDataResponse: ServerSideResponse<TestRecord> = {
|
|
407
|
+
data: [{ id: '4', name: 'Server Data', value: 40 }],
|
|
408
|
+
totalCount: 1
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const mockFetchData = vi.fn().mockResolvedValue(serverDataResponse);
|
|
412
|
+
|
|
413
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
414
|
+
fetchData: mockFetchData
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const { result } = renderHook(() =>
|
|
418
|
+
useDataTablePerformance({
|
|
419
|
+
data: mockData,
|
|
420
|
+
serverSide
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
await result.current.fetchServerData({ page: 1, pageSize: 10 });
|
|
425
|
+
|
|
426
|
+
await waitFor(
|
|
427
|
+
() => {
|
|
428
|
+
expect(result.current.serverData).not.toBeNull();
|
|
429
|
+
},
|
|
430
|
+
{ timeout: 2000 }
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
expect(result.current.processedData).toEqual(serverDataResponse.data);
|
|
434
|
+
expect(result.current.totalCount).toBe(1);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('Error Handling and Recovery', () => {
|
|
439
|
+
it('initializes with no errors', () => {
|
|
440
|
+
const { result } = renderHook(() =>
|
|
441
|
+
useDataTablePerformance({
|
|
442
|
+
data: mockData
|
|
443
|
+
})
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(result.current.errorState.hasErrors).toBe(false);
|
|
447
|
+
expect(result.current.errorState.errorCount).toBe(0);
|
|
448
|
+
expect(result.current.errorState.lastError).toBe(null);
|
|
449
|
+
expect(result.current.errorState.fallbacksActive).toEqual([]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('clears errors when clearErrors is called', () => {
|
|
453
|
+
const { result } = renderHook(() =>
|
|
454
|
+
useDataTablePerformance({
|
|
455
|
+
data: mockData
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
result.current.clearErrors();
|
|
460
|
+
|
|
461
|
+
expect(result.current.errorState.hasErrors).toBe(false);
|
|
462
|
+
expect(result.current.errorState.errorCount).toBe(0);
|
|
463
|
+
expect(result.current.errorState.lastError).toBe(null);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('retries last failed operation when retryLastOperation is called', async () => {
|
|
467
|
+
// The retryLastOperation function requires lastFailedOperation to be set
|
|
468
|
+
// which happens internally through handleError. This test verifies the function exists
|
|
469
|
+
// and can be called without error when there's no failed operation
|
|
470
|
+
const { result } = renderHook(() =>
|
|
471
|
+
useDataTablePerformance({
|
|
472
|
+
data: mockData,
|
|
473
|
+
enableErrorRecovery: true
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// When there's no lastFailedOperation, the function should complete without error
|
|
478
|
+
await act(async () => {
|
|
479
|
+
await result.current.retryLastOperation();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Function should exist and be callable
|
|
483
|
+
expect(typeof result.current.retryLastOperation).toBe('function');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('handles retry failure gracefully', async () => {
|
|
487
|
+
// This test verifies that retryLastOperation handles errors gracefully
|
|
488
|
+
// Since lastFailedOperation is internal state, we test the function's existence
|
|
489
|
+
// and that it can be called without throwing
|
|
490
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
491
|
+
|
|
492
|
+
const { result } = renderHook(() =>
|
|
493
|
+
useDataTablePerformance({
|
|
494
|
+
data: mockData
|
|
495
|
+
})
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Call retryLastOperation - should complete without error even if no operation to retry
|
|
499
|
+
await act(async () => {
|
|
500
|
+
await result.current.retryLastOperation();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Function should exist and be callable
|
|
504
|
+
expect(typeof result.current.retryLastOperation).toBe('function');
|
|
505
|
+
|
|
506
|
+
consoleSpy.mockRestore();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('Memory Monitoring', () => {
|
|
511
|
+
it('tracks memory usage', () => {
|
|
512
|
+
const { result } = renderHook(() =>
|
|
513
|
+
useDataTablePerformance({
|
|
514
|
+
data: mockData
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(result.current.memoryUsage).toBeGreaterThanOrEqual(0);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('updates memory usage when data changes', () => {
|
|
522
|
+
const { result, rerender } = renderHook(
|
|
523
|
+
({ data }) =>
|
|
524
|
+
useDataTablePerformance({
|
|
525
|
+
data
|
|
526
|
+
}),
|
|
527
|
+
{
|
|
528
|
+
initialProps: { data: mockData }
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const initialUsage = result.current.memoryUsage;
|
|
533
|
+
|
|
534
|
+
const largerData = Array.from({ length: 100 }, (_, i) => ({
|
|
535
|
+
id: String(i),
|
|
536
|
+
name: `Test ${i}`,
|
|
537
|
+
value: i
|
|
538
|
+
}));
|
|
539
|
+
|
|
540
|
+
rerender({ data: largerData });
|
|
541
|
+
|
|
542
|
+
// Memory usage should reflect the larger dataset
|
|
543
|
+
expect(result.current.memoryUsage).toBeDefined();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe('Data Processing', () => {
|
|
548
|
+
it('returns original data when no search query', () => {
|
|
549
|
+
const { result } = renderHook(() =>
|
|
550
|
+
useDataTablePerformance({
|
|
551
|
+
data: mockData
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
expect(result.current.processedData).toEqual(mockData);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('returns correct total count for client data', () => {
|
|
559
|
+
const { result } = renderHook(() =>
|
|
560
|
+
useDataTablePerformance({
|
|
561
|
+
data: mockData
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
expect(result.current.totalCount).toBe(3);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('returns correct total count for server data', async () => {
|
|
569
|
+
const serverDataResponse: ServerSideResponse<TestRecord> = {
|
|
570
|
+
data: mockData,
|
|
571
|
+
totalCount: 50
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const mockFetchData = vi.fn().mockResolvedValue(serverDataResponse);
|
|
575
|
+
|
|
576
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
577
|
+
fetchData: mockFetchData
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const { result } = renderHook(() =>
|
|
581
|
+
useDataTablePerformance({
|
|
582
|
+
data: [],
|
|
583
|
+
serverSide
|
|
584
|
+
})
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
await result.current.fetchServerData({ page: 1, pageSize: 10 });
|
|
588
|
+
|
|
589
|
+
await waitFor(
|
|
590
|
+
() => {
|
|
591
|
+
expect(result.current.totalCount).toBe(50);
|
|
592
|
+
},
|
|
593
|
+
{ timeout: 2000 }
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('Loading States', () => {
|
|
599
|
+
it('initializes with loading false', () => {
|
|
600
|
+
const { result } = renderHook(() =>
|
|
601
|
+
useDataTablePerformance({
|
|
602
|
+
data: mockData
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
expect(result.current.isLoading).toBe(false);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('sets loading to true during server data fetch', async () => {
|
|
610
|
+
let resolveFetch: (value: any) => void;
|
|
611
|
+
const mockFetchData = vi.fn().mockImplementation(() => {
|
|
612
|
+
return new Promise((resolve) => {
|
|
613
|
+
resolveFetch = resolve;
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
618
|
+
fetchData: mockFetchData
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const { result } = renderHook(() =>
|
|
622
|
+
useDataTablePerformance({
|
|
623
|
+
data: [],
|
|
624
|
+
serverSide
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const fetchPromise = act(async () => {
|
|
629
|
+
await result.current.fetchServerData({ page: 1, pageSize: 10 });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Loading should be true during fetch (may need to wait briefly)
|
|
633
|
+
await waitFor(
|
|
634
|
+
() => {
|
|
635
|
+
expect(result.current.isLoading).toBe(true);
|
|
636
|
+
},
|
|
637
|
+
{ timeout: 500 }
|
|
638
|
+
).catch(() => {
|
|
639
|
+
// If loading completes too quickly, that's also acceptable
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
resolveFetch!({ data: mockData, totalCount: 3 });
|
|
643
|
+
await fetchPromise;
|
|
644
|
+
|
|
645
|
+
await waitFor(
|
|
646
|
+
() => {
|
|
647
|
+
expect(result.current.isLoading).toBe(false);
|
|
648
|
+
},
|
|
649
|
+
{ timeout: 2000 }
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe('Cleanup', () => {
|
|
655
|
+
it('provides cleanup function', () => {
|
|
656
|
+
const { result } = renderHook(() =>
|
|
657
|
+
useDataTablePerformance({
|
|
658
|
+
data: mockData
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
expect(typeof result.current.cleanup).toBe('function');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('cleans up resources when cleanup is called', () => {
|
|
666
|
+
const { result } = renderHook(() =>
|
|
667
|
+
useDataTablePerformance({
|
|
668
|
+
data: mockData,
|
|
669
|
+
chunking: {
|
|
670
|
+
enabled: true,
|
|
671
|
+
chunkSize: 100
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Cleanup should work even if refs are null (uses optional chaining)
|
|
677
|
+
expect(() => result.current.cleanup()).not.toThrow();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('cleans up on unmount', () => {
|
|
681
|
+
const { unmount } = renderHook(() =>
|
|
682
|
+
useDataTablePerformance({
|
|
683
|
+
data: mockData
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
expect(() => unmount()).not.toThrow();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe('Integration', () => {
|
|
692
|
+
it('handles all features together', async () => {
|
|
693
|
+
const mockFetchData = vi.fn().mockResolvedValue({
|
|
694
|
+
data: mockData,
|
|
695
|
+
totalCount: 100
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const serverSide: ServerSideConfig<TestRecord> = {
|
|
699
|
+
fetchData: mockFetchData
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const { result } = renderHook(() =>
|
|
703
|
+
useDataTablePerformance({
|
|
704
|
+
data: mockData,
|
|
705
|
+
performance: {
|
|
706
|
+
serverSideThreshold: 1000
|
|
707
|
+
},
|
|
708
|
+
serverSide,
|
|
709
|
+
searchIndex: {
|
|
710
|
+
enabled: true
|
|
711
|
+
},
|
|
712
|
+
chunking: {
|
|
713
|
+
enabled: true,
|
|
714
|
+
chunkSize: 50
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
// Test pagination
|
|
720
|
+
expect(result.current.paginationMode).toBe('server');
|
|
721
|
+
expect(result.current.pageSizeOptions).toBeDefined();
|
|
722
|
+
|
|
723
|
+
// Test search
|
|
724
|
+
act(() => {
|
|
725
|
+
result.current.setSearchQuery('test');
|
|
726
|
+
});
|
|
727
|
+
expect(result.current.searchQuery).toBe('test');
|
|
728
|
+
|
|
729
|
+
// Test server fetch
|
|
730
|
+
await result.current.fetchServerData({ page: 1, pageSize: 10 });
|
|
731
|
+
|
|
732
|
+
await waitFor(
|
|
733
|
+
() => {
|
|
734
|
+
expect(result.current.isLoading).toBe(false);
|
|
735
|
+
},
|
|
736
|
+
{ timeout: 2000 }
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// Test error handling
|
|
740
|
+
result.current.clearErrors();
|
|
741
|
+
expect(result.current.errorState.hasErrors).toBe(false);
|
|
742
|
+
|
|
743
|
+
// Test cleanup
|
|
744
|
+
expect(() => result.current.cleanup()).not.toThrow();
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|