@jmruthers/pace-core 0.5.75 → 0.5.76

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.
Files changed (226) hide show
  1. package/dist/{DataTable-HWZQGASI.js → DataTable-4GAVPIEG.js} +48 -30
  2. package/dist/{PublicLoadingSpinner-BKNBT6b6.d.ts → PublicLoadingSpinner-BiNER8F5.d.ts} +28 -17
  3. package/dist/{chunk-33PHABLB.js → chunk-AFGTSUAD.js} +10 -127
  4. package/dist/chunk-AFGTSUAD.js.map +1 -0
  5. package/dist/{chunk-2DFZ432F.js → chunk-K34IM5CT.js} +3 -5
  6. package/dist/{chunk-2DFZ432F.js.map → chunk-K34IM5CT.js.map} +1 -1
  7. package/dist/{chunk-2CHATWBF.js → chunk-KHJS6VIA.js} +199 -35
  8. package/dist/chunk-KHJS6VIA.js.map +1 -0
  9. package/dist/{chunk-ZTT2AXMX.js → chunk-KK73ZB4E.js} +2 -2
  10. package/dist/{chunk-CY3AHGO4.js → chunk-M5IWZRBT.js} +1750 -2815
  11. package/dist/chunk-M5IWZRBT.js.map +1 -0
  12. package/dist/{chunk-DAXLNIDY.js → chunk-Y6TXWPJO.js} +6 -4
  13. package/dist/{chunk-DAXLNIDY.js.map → chunk-Y6TXWPJO.js.map} +1 -1
  14. package/dist/{chunk-YNUBMSMV.js → chunk-YCKPEMJA.js} +186 -263
  15. package/dist/chunk-YCKPEMJA.js.map +1 -0
  16. package/dist/components.d.ts +1 -1
  17. package/dist/components.js +7 -6
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.d.ts +17 -40
  20. package/dist/hooks.js +6 -6
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +12 -10
  23. package/dist/index.js.map +1 -1
  24. package/dist/rbac/index.d.ts +54 -1
  25. package/dist/rbac/index.js +5 -4
  26. package/dist/utils.js +1 -1
  27. package/docs/TERMINOLOGY.md +231 -0
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/MissingUserContextError.md +1 -1
  32. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  33. package/docs/api/classes/PermissionDeniedError.md +1 -1
  34. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +1 -1
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +1 -1
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/interfaces/AggregateConfig.md +1 -1
  44. package/docs/api/interfaces/ButtonProps.md +1 -1
  45. package/docs/api/interfaces/CardProps.md +1 -1
  46. package/docs/api/interfaces/ColorPalette.md +1 -1
  47. package/docs/api/interfaces/ColorShade.md +1 -1
  48. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  49. package/docs/api/interfaces/DataTableAction.md +1 -1
  50. package/docs/api/interfaces/DataTableColumn.md +1 -1
  51. package/docs/api/interfaces/DataTableProps.md +1 -1
  52. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  53. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  54. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/EventLogoProps.md +1 -1
  56. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  57. package/docs/api/interfaces/FileMetadata.md +1 -1
  58. package/docs/api/interfaces/FileReference.md +1 -1
  59. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  60. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  61. package/docs/api/interfaces/FileUploadProps.md +1 -1
  62. package/docs/api/interfaces/FooterProps.md +1 -1
  63. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  64. package/docs/api/interfaces/InputProps.md +1 -1
  65. package/docs/api/interfaces/LabelProps.md +1 -1
  66. package/docs/api/interfaces/LoginFormProps.md +1 -1
  67. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  68. package/docs/api/interfaces/NavigationContextType.md +1 -1
  69. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  70. package/docs/api/interfaces/NavigationItem.md +1 -1
  71. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  73. package/docs/api/interfaces/Organisation.md +1 -1
  74. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  75. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  76. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  77. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  78. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  79. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  80. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  81. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  82. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  83. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  84. package/docs/api/interfaces/PaletteData.md +1 -1
  85. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACContextType.md +1 -1
  94. package/docs/api/interfaces/RBACLogger.md +1 -1
  95. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  99. package/docs/api/interfaces/RouteConfig.md +1 -1
  100. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  101. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  102. package/docs/api/interfaces/StorageConfig.md +1 -1
  103. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  104. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  105. package/docs/api/interfaces/StorageListOptions.md +1 -1
  106. package/docs/api/interfaces/StorageListResult.md +1 -1
  107. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  108. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  109. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  110. package/docs/api/interfaces/StyleImport.md +1 -1
  111. package/docs/api/interfaces/SwitchProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +47 -0
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +47 -0
  125. package/docs/api/interfaces/UserEventAccess.md +1 -1
  126. package/docs/api/interfaces/UserMenuProps.md +1 -1
  127. package/docs/api/interfaces/UserProfile.md +1 -1
  128. package/docs/api/modules.md +57 -11
  129. package/docs/api-reference/providers.md +26 -7
  130. package/docs/best-practices/README.md +20 -0
  131. package/docs/best-practices/accessibility.md +566 -0
  132. package/docs/best-practices/performance-expansion.md +473 -0
  133. package/docs/core-concepts/authentication.md +15 -7
  134. package/docs/documentation-index.md +1 -1
  135. package/docs/documentation-templates.md +539 -0
  136. package/docs/getting-started/quick-start.md +16 -66
  137. package/docs/implementation-guides/component-styling.md +410 -0
  138. package/docs/implementation-guides/data-tables.md +1 -1
  139. package/docs/style-guide.md +39 -0
  140. package/package.json +1 -1
  141. package/src/__tests__/TEST_GUIDE_CURSOR.md +290 -0
  142. package/src/__tests__/helpers/supabaseMock.ts +48 -2
  143. package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +17 -6
  144. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +73 -9
  145. package/src/components/DataTable/components/DataTableCore.tsx +280 -475
  146. package/src/components/DataTable/components/UnifiedTableBody.tsx +120 -153
  147. package/src/components/DataTable/components/index.ts +1 -2
  148. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +208 -275
  149. package/src/components/DataTable/core/index.ts +1 -8
  150. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +525 -0
  151. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +570 -0
  152. package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +214 -0
  153. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  154. package/src/components/DataTable/hooks/index.ts +6 -0
  155. package/src/components/DataTable/hooks/useColumnReordering.ts +1 -0
  156. package/src/components/DataTable/hooks/useDataTablePermissions.ts +149 -0
  157. package/src/components/DataTable/hooks/useDataTableState.ts +12 -6
  158. package/src/components/DataTable/hooks/useHierarchicalState.ts +26 -8
  159. package/src/components/DataTable/hooks/useTableColumns.ts +153 -0
  160. package/src/components/DataTable/index.ts +1 -9
  161. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +89 -0
  162. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +3 -6
  163. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +462 -0
  164. package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +247 -0
  165. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +8 -6
  166. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +466 -0
  167. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +265 -0
  168. package/src/components/DataTable/utils/errorHandling.ts +52 -460
  169. package/src/components/DataTable/utils/exportUtils.ts +46 -15
  170. package/src/components/DataTable/utils/hierarchicalSorting.ts +50 -3
  171. package/src/components/DataTable/utils/hierarchicalUtils.ts +167 -34
  172. package/src/components/DataTable/utils/index.ts +5 -0
  173. package/src/components/DataTable/utils/rowUtils.ts +68 -0
  174. package/src/components/EventSelector/EventSelector.test.tsx +672 -0
  175. package/src/components/Label/__tests__/Label.test.tsx +434 -0
  176. package/src/components/PublicLayout/__tests__/PublicPageContextChecker.test.tsx +190 -0
  177. package/src/components/PublicLayout/__tests__/PublicPageDebugger.test.tsx +185 -0
  178. package/src/components/PublicLayout/__tests__/PublicPageProvider.test.tsx +313 -0
  179. package/src/components/Select/Select.test.tsx +143 -120
  180. package/src/components/Select/Select.tsx +47 -212
  181. package/src/components/Select/hooks.ts +36 -1
  182. package/src/components/Select/index.ts +2 -1
  183. package/src/hooks/services/__tests__/useServiceHooks.test.tsx +137 -0
  184. package/src/hooks/useSecureDataAccess.test.ts +32 -29
  185. package/src/providers/__tests__/ProviderLifecycle.test.tsx +341 -0
  186. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +437 -0
  187. package/src/rbac/hooks/index.ts +2 -0
  188. package/src/rbac/hooks/useResolvedScope.ts +232 -0
  189. package/src/services/__tests__/InactivityService.lifecycle.test.ts +411 -0
  190. package/src/services/__tests__/OrganisationService.pagination.test.ts +375 -0
  191. package/src/types/__tests__/README.md +114 -0
  192. package/src/types/__tests__/validation.test.ts +731 -0
  193. package/src/utils/__tests__/file-reference.test.ts +383 -0
  194. package/src/utils/__tests__/performanceBenchmark.test.ts +175 -0
  195. package/src/utils/appNameResolver.test.ts +54 -0
  196. package/src/validation/__tests__/csrf.unit.test.ts +63 -0
  197. package/src/validation/__tests__/passwordSchema.unit.test.ts +105 -0
  198. package/dist/chunk-2CHATWBF.js.map +0 -1
  199. package/dist/chunk-33PHABLB.js.map +0 -1
  200. package/dist/chunk-CY3AHGO4.js.map +0 -1
  201. package/dist/chunk-TYHR5X4W.js +0 -33
  202. package/dist/chunk-TYHR5X4W.js.map +0 -1
  203. package/dist/chunk-YNUBMSMV.js.map +0 -1
  204. package/dist/eventContext-BBA42P6G.js +0 -14
  205. package/dist/eventContext-BBA42P6G.js.map +0 -1
  206. package/docs/documentation-style-checklist.md +0 -294
  207. package/src/components/DataTable/components/DataTableBody.tsx +0 -488
  208. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -144
  209. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -515
  210. package/src/components/DataTable/core/ActionManager.ts +0 -235
  211. package/src/components/DataTable/core/ColumnManager.ts +0 -215
  212. package/src/components/DataTable/core/DataManager.ts +0 -188
  213. package/src/components/DataTable/core/DataTableContext.tsx +0 -181
  214. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -264
  215. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  216. package/src/components/DataTable/core/StateManager.ts +0 -311
  217. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -634
  218. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -193
  219. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -519
  220. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -714
  221. package/src/components/DataTable/core/interfaces.ts +0 -338
  222. package/src/components/DataTable/utils/debugTools.ts +0 -583
  223. package/src/components/Select/Select.bug-test.tsx +0 -69
  224. package/src/components/Select/Select.refactored.tsx +0 -497
  225. /package/dist/{DataTable-HWZQGASI.js.map → DataTable-4GAVPIEG.js.map} +0 -0
  226. /package/dist/{chunk-ZTT2AXMX.js.map → chunk-KK73ZB4E.js.map} +0 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * @file Unit Tests for useColumnOrderPersistence Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Hooks/__tests__
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import { renderHook, waitFor, act } from '@testing-library/react';
9
+ import { useColumnOrderPersistence } from '../useColumnOrderPersistence';
10
+
11
+ describe('[unit] useColumnOrderPersistence', () => {
12
+ beforeEach(() => {
13
+ localStorage.clear();
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ describe('Initialization', () => {
22
+ it('initializes with default order when no saved order exists', async () => {
23
+ const defaultOrder = ['col1', 'col2', 'col3'];
24
+ const { result } = renderHook(() => useColumnOrderPersistence({
25
+ tableId: 'test',
26
+ defaultOrder,
27
+ enablePersistence: true
28
+ }));
29
+
30
+ await waitFor(() => {
31
+ expect(result.current.isLoaded).toBe(true);
32
+ });
33
+
34
+ expect(result.current.columnOrder).toEqual(defaultOrder);
35
+ });
36
+
37
+ it('loads saved order from localStorage on mount', async () => {
38
+ const savedOrder = ['col3', 'col1', 'col2'];
39
+ localStorage.setItem('datatable-column-order-test', JSON.stringify(savedOrder));
40
+
41
+ const { result } = renderHook(() => useColumnOrderPersistence({
42
+ tableId: 'test',
43
+ defaultOrder: ['col1', 'col2', 'col3'],
44
+ enablePersistence: true
45
+ }));
46
+
47
+ await waitFor(() => {
48
+ expect(result.current.isLoaded).toBe(true);
49
+ });
50
+
51
+ expect(result.current.columnOrder).toEqual(savedOrder);
52
+ });
53
+
54
+ it('generates unique storage key based on tableId', async () => {
55
+ const savedOrder = ['col1'];
56
+ localStorage.setItem('datatable-column-order-unique-table', JSON.stringify(savedOrder));
57
+
58
+ const { result } = renderHook(() => useColumnOrderPersistence({
59
+ tableId: 'unique-table',
60
+ defaultOrder: ['col2'],
61
+ enablePersistence: true
62
+ }));
63
+
64
+ await waitFor(() => {
65
+ expect(result.current.isLoaded).toBe(true);
66
+ });
67
+
68
+ expect(result.current.columnOrder).toEqual(savedOrder);
69
+ });
70
+
71
+ it('uses default order when tableId is not provided (no persistence)', async () => {
72
+ const defaultOrder = ['col1'];
73
+ localStorage.setItem('datatable-column-order', JSON.stringify(['col2']));
74
+
75
+ const { result } = renderHook(() => useColumnOrderPersistence({
76
+ defaultOrder,
77
+ enablePersistence: true
78
+ }));
79
+
80
+ await waitFor(() => {
81
+ expect(result.current.isLoaded).toBe(true);
82
+ });
83
+
84
+ // When tableId is not provided, it doesn't load from localStorage
85
+ expect(result.current.columnOrder).toEqual(defaultOrder);
86
+ });
87
+ });
88
+
89
+ describe('Persistence', () => {
90
+ it('saves column order to localStorage when updated', async () => {
91
+ const newOrder = ['col3', 'col2', 'col1'];
92
+ const { result } = renderHook(() => useColumnOrderPersistence({
93
+ tableId: 'test',
94
+ defaultOrder: ['col1', 'col2', 'col3'],
95
+ enablePersistence: true
96
+ }));
97
+
98
+ await waitFor(() => {
99
+ expect(result.current.isLoaded).toBe(true);
100
+ });
101
+
102
+ act(() => {
103
+ result.current.updateColumnOrder(newOrder);
104
+ });
105
+
106
+ expect(result.current.columnOrder).toEqual(newOrder);
107
+
108
+ const saved = localStorage.getItem('datatable-column-order-test');
109
+ expect(saved).toBe(JSON.stringify(newOrder));
110
+ });
111
+
112
+ it('does not persist when enablePersistence is false', async () => {
113
+ const { result } = renderHook(() => useColumnOrderPersistence({
114
+ tableId: 'test',
115
+ defaultOrder: ['col1', 'col2'],
116
+ enablePersistence: false
117
+ }));
118
+
119
+ await waitFor(() => {
120
+ expect(result.current.isLoaded).toBe(true);
121
+ });
122
+
123
+ act(() => {
124
+ result.current.updateColumnOrder(['col2', 'col1']);
125
+ });
126
+
127
+ expect(result.current.columnOrder).toEqual(['col2', 'col1']);
128
+ expect(localStorage.getItem('datatable-column-order-test')).toBeNull();
129
+ });
130
+
131
+ it('does not persist when tableId is missing', async () => {
132
+ const { result } = renderHook(() => useColumnOrderPersistence({
133
+ defaultOrder: ['col1', 'col2'],
134
+ enablePersistence: true
135
+ }));
136
+
137
+ await waitFor(() => {
138
+ expect(result.current.isLoaded).toBe(true);
139
+ });
140
+
141
+ act(() => {
142
+ result.current.updateColumnOrder(['col2', 'col1']);
143
+ });
144
+
145
+ expect(result.current.columnOrder).toEqual(['col2', 'col1']);
146
+ // Verify no persistence attempted
147
+ const allKeys = Object.keys(localStorage);
148
+ expect(allKeys.filter(k => k.startsWith('datatable-column-order'))).toEqual([]);
149
+ });
150
+ });
151
+
152
+ describe('Error Handling', () => {
153
+ it('handles invalid JSON in localStorage gracefully', async () => {
154
+ localStorage.setItem('datatable-column-order-test', 'invalid-json');
155
+
156
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
157
+
158
+ const { result } = renderHook(() => useColumnOrderPersistence({
159
+ tableId: 'test',
160
+ defaultOrder: ['col1', 'col2'],
161
+ enablePersistence: true
162
+ }));
163
+
164
+ await waitFor(() => {
165
+ expect(result.current.isLoaded).toBe(true);
166
+ });
167
+
168
+ expect(result.current.columnOrder).toEqual(['col1', 'col2']);
169
+ expect(consoleWarnSpy).toHaveBeenCalled();
170
+
171
+ consoleWarnSpy.mockRestore();
172
+ });
173
+
174
+ it('handles localStorage quota exceeded error gracefully', async () => {
175
+ // Mock setItem to throw quota error
176
+ const originalSetItem = Storage.prototype.setItem;
177
+ Storage.prototype.setItem = vi.fn(() => {
178
+ throw new DOMException('QuotaExceededError', 'QuotaExceededError');
179
+ });
180
+
181
+ const { result } = renderHook(() => useColumnOrderPersistence({
182
+ tableId: 'test',
183
+ defaultOrder: ['col1', 'col2'],
184
+ enablePersistence: true
185
+ }));
186
+
187
+ await waitFor(() => {
188
+ expect(result.current.isLoaded).toBe(true);
189
+ });
190
+
191
+ // Should not throw, gracefully handles the error
192
+ act(() => {
193
+ result.current.updateColumnOrder(['col2', 'col1']);
194
+ });
195
+
196
+ expect(result.current.columnOrder).toEqual(['col2', 'col1']);
197
+
198
+ Storage.prototype.setItem = originalSetItem;
199
+ });
200
+
201
+ it('handles localStorage setItem failure gracefully', async () => {
202
+ // Mock setItem to throw error
203
+ const originalSetItem = Storage.prototype.setItem;
204
+ Storage.prototype.setItem = vi.fn(() => {
205
+ throw new Error('Storage failure');
206
+ });
207
+
208
+ const { result } = renderHook(() => useColumnOrderPersistence({
209
+ tableId: 'test',
210
+ defaultOrder: ['col1', 'col2'],
211
+ enablePersistence: true
212
+ }));
213
+
214
+ await waitFor(() => {
215
+ expect(result.current.isLoaded).toBe(true);
216
+ });
217
+
218
+ act(() => {
219
+ result.current.updateColumnOrder(['col2', 'col1']);
220
+ });
221
+
222
+ expect(result.current.columnOrder).toEqual(['col2', 'col1']);
223
+
224
+ Storage.prototype.setItem = originalSetItem;
225
+ });
226
+
227
+ it('handles non-array saved data', async () => {
228
+ localStorage.setItem('datatable-column-order-test', JSON.stringify({ invalid: 'data' }));
229
+
230
+ const { result } = renderHook(() => useColumnOrderPersistence({
231
+ tableId: 'test',
232
+ defaultOrder: ['col1', 'col2'],
233
+ enablePersistence: true
234
+ }));
235
+
236
+ await waitFor(() => {
237
+ expect(result.current.isLoaded).toBe(true);
238
+ });
239
+
240
+ // Should fall back to defaultOrder
241
+ expect(result.current.columnOrder).toEqual(['col1', 'col2']);
242
+ });
243
+ });
244
+
245
+ describe('Reset Functionality', () => {
246
+ it('resets to default order correctly', async () => {
247
+ const savedOrder = ['col3', 'col2', 'col1'];
248
+ localStorage.setItem('datatable-column-order-test', JSON.stringify(savedOrder));
249
+
250
+ const defaultOrder = ['col1', 'col2', 'col3'];
251
+ const { result } = renderHook(() => useColumnOrderPersistence({
252
+ tableId: 'test',
253
+ defaultOrder,
254
+ enablePersistence: true
255
+ }));
256
+
257
+ await waitFor(() => {
258
+ expect(result.current.isLoaded).toBe(true);
259
+ });
260
+
261
+ expect(result.current.columnOrder).toEqual(savedOrder);
262
+
263
+ act(() => {
264
+ result.current.resetColumnOrder();
265
+ });
266
+
267
+ expect(result.current.columnOrder).toEqual(defaultOrder);
268
+ expect(localStorage.getItem('datatable-column-order-test')).toBeNull();
269
+ });
270
+
271
+ it('removes saved order from localStorage when reset', async () => {
272
+ const savedOrder = ['col3', 'col2', 'col1'];
273
+ localStorage.setItem('datatable-column-order-test', JSON.stringify(savedOrder));
274
+
275
+ const { result } = renderHook(() => useColumnOrderPersistence({
276
+ tableId: 'test',
277
+ defaultOrder: ['col1', 'col2', 'col3'],
278
+ enablePersistence: true
279
+ }));
280
+
281
+ await waitFor(() => {
282
+ expect(result.current.isLoaded).toBe(true);
283
+ });
284
+
285
+ act(() => {
286
+ result.current.resetColumnOrder();
287
+ });
288
+
289
+ expect(localStorage.getItem('datatable-column-order-test')).toBeNull();
290
+ });
291
+
292
+ it('handles localStorage removeItem failure gracefully', async () => {
293
+ // Mock removeItem to throw error
294
+ const originalRemoveItem = Storage.prototype.removeItem;
295
+ Storage.prototype.removeItem = vi.fn(() => {
296
+ throw new Error('Remove failed');
297
+ });
298
+
299
+ const defaultOrder = ['col1', 'col2'];
300
+ const { result } = renderHook(() => useColumnOrderPersistence({
301
+ tableId: 'test',
302
+ defaultOrder,
303
+ enablePersistence: true
304
+ }));
305
+
306
+ await waitFor(() => {
307
+ expect(result.current.isLoaded).toBe(true);
308
+ });
309
+
310
+ act(() => {
311
+ result.current.resetColumnOrder();
312
+ });
313
+
314
+ expect(result.current.columnOrder).toEqual(defaultOrder);
315
+
316
+ Storage.prototype.removeItem = originalRemoveItem;
317
+ });
318
+ });
319
+
320
+ describe('Clear All Preferences', () => {
321
+ it('clears all datatable preferences', async () => {
322
+ localStorage.setItem('datatable-column-order-table1', JSON.stringify(['col1']));
323
+ localStorage.setItem('datatable-column-order-table2', JSON.stringify(['col2']));
324
+ localStorage.setItem('datatable-other-pref', JSON.stringify({ some: 'pref' }));
325
+ localStorage.setItem('other-storage-key', JSON.stringify({ other: 'data' }));
326
+
327
+ const { result } = renderHook(() => useColumnOrderPersistence({
328
+ tableId: 'test',
329
+ enablePersistence: true
330
+ }));
331
+
332
+ await waitFor(() => {
333
+ expect(result.current.isLoaded).toBe(true);
334
+ });
335
+
336
+ act(() => {
337
+ result.current.clearAllPreferences();
338
+ });
339
+
340
+ expect(localStorage.getItem('datatable-column-order-table1')).toBeNull();
341
+ expect(localStorage.getItem('datatable-column-order-table2')).toBeNull();
342
+ expect(localStorage.getItem('datatable-other-pref')).toBeNull();
343
+ expect(localStorage.getItem('other-storage-key')).not.toBeNull(); // Should keep non-datatable keys
344
+ });
345
+
346
+ it('does not clear when enablePersistence is false', async () => {
347
+ localStorage.setItem('datatable-column-order-table1', JSON.stringify(['col1']));
348
+
349
+ const { result } = renderHook(() => useColumnOrderPersistence({
350
+ tableId: 'test',
351
+ enablePersistence: false
352
+ }));
353
+
354
+ await waitFor(() => {
355
+ expect(result.current.isLoaded).toBe(true);
356
+ });
357
+
358
+ act(() => {
359
+ result.current.clearAllPreferences();
360
+ });
361
+
362
+ // Should not clear anything when persistence is disabled
363
+ expect(localStorage.getItem('datatable-column-order-table1')).not.toBeNull();
364
+ });
365
+
366
+ it('handles clearAllPreferences failure gracefully', async () => {
367
+ localStorage.setItem('datatable-column-order-table1', JSON.stringify(['col1']));
368
+ localStorage.setItem('datatable-other-pref', JSON.stringify({ some: 'data' }));
369
+
370
+ // Mock removeItem to throw error
371
+ const originalRemoveItem = Storage.prototype.removeItem;
372
+ Storage.prototype.removeItem = vi.fn(() => {
373
+ throw new Error('Clear failed');
374
+ });
375
+
376
+ const { result } = renderHook(() => useColumnOrderPersistence({
377
+ tableId: 'test',
378
+ enablePersistence: true
379
+ }));
380
+
381
+ await waitFor(() => {
382
+ expect(result.current.isLoaded).toBe(true);
383
+ });
384
+
385
+ // Should not throw
386
+ act(() => {
387
+ result.current.clearAllPreferences();
388
+ });
389
+
390
+ // Should complete successfully despite storage error
391
+ expect(result.current).toBeDefined();
392
+
393
+ Storage.prototype.removeItem = originalRemoveItem;
394
+ });
395
+ });
396
+
397
+ describe('State Updates', () => {
398
+ it('updates state immediately when updateColumnOrder is called', async () => {
399
+ const { result } = renderHook(() => useColumnOrderPersistence({
400
+ tableId: 'test',
401
+ defaultOrder: ['col1', 'col2', 'col3'],
402
+ enablePersistence: true
403
+ }));
404
+
405
+ await waitFor(() => {
406
+ expect(result.current.isLoaded).toBe(true);
407
+ });
408
+
409
+ act(() => {
410
+ result.current.updateColumnOrder(['col3', 'col1', 'col2']);
411
+ });
412
+
413
+ expect(result.current.columnOrder).toEqual(['col3', 'col1', 'col2']);
414
+ });
415
+
416
+ it('persists state across hook re-renders', async () => {
417
+ const { result, rerender } = renderHook(() => useColumnOrderPersistence({
418
+ tableId: 'test',
419
+ defaultOrder: ['col1', 'col2'],
420
+ enablePersistence: true
421
+ }));
422
+
423
+ await waitFor(() => {
424
+ expect(result.current.isLoaded).toBe(true);
425
+ });
426
+
427
+ act(() => {
428
+ result.current.updateColumnOrder(['col2', 'col1']);
429
+ });
430
+
431
+ rerender();
432
+
433
+ await waitFor(() => {
434
+ expect(result.current.columnOrder).toEqual(['col2', 'col1']);
435
+ });
436
+ });
437
+ });
438
+
439
+ describe('Edge Cases', () => {
440
+ it('handles empty default order', async () => {
441
+ const { result } = renderHook(() => useColumnOrderPersistence({
442
+ tableId: 'test',
443
+ defaultOrder: [],
444
+ enablePersistence: true
445
+ }));
446
+
447
+ await waitFor(() => {
448
+ expect(result.current.isLoaded).toBe(true);
449
+ });
450
+
451
+ expect(result.current.columnOrder).toEqual([]);
452
+ });
453
+
454
+ it('handles empty saved order', async () => {
455
+ localStorage.setItem('datatable-column-order-test', JSON.stringify([]));
456
+
457
+ const { result } = renderHook(() => useColumnOrderPersistence({
458
+ tableId: 'test',
459
+ defaultOrder: ['col1', 'col2'],
460
+ enablePersistence: true
461
+ }));
462
+
463
+ await waitFor(() => {
464
+ expect(result.current.isLoaded).toBe(true);
465
+ });
466
+
467
+ expect(result.current.columnOrder).toEqual([]);
468
+ });
469
+
470
+ it('handles null saved data', async () => {
471
+ localStorage.setItem('datatable-column-order-test', JSON.stringify(null));
472
+
473
+ const { result } = renderHook(() => useColumnOrderPersistence({
474
+ tableId: 'test',
475
+ defaultOrder: ['col1', 'col2'],
476
+ enablePersistence: true
477
+ }));
478
+
479
+ await waitFor(() => {
480
+ expect(result.current.isLoaded).toBe(true);
481
+ });
482
+
483
+ // Should handle gracefully
484
+ expect(result.current.isLoaded).toBe(true);
485
+ });
486
+ });
487
+
488
+ describe('Load State', () => {
489
+ it('returns isLoaded=true after loading completes', async () => {
490
+ const { result } = renderHook(() => useColumnOrderPersistence({
491
+ tableId: 'test',
492
+ defaultOrder: ['col1'],
493
+ enablePersistence: true
494
+ }));
495
+
496
+ // isLoaded starts as false only for the brief moment during effect
497
+ // Due to the synchronous nature in tests, it may already be true
498
+ expect(typeof result.current.isLoaded).toBe('boolean');
499
+
500
+ await waitFor(() => {
501
+ expect(result.current.isLoaded).toBe(true);
502
+ });
503
+ });
504
+
505
+ it('returns isLoaded=true immediately when persistence is disabled', () => {
506
+ const { result } = renderHook(() => useColumnOrderPersistence({
507
+ tableId: 'test',
508
+ defaultOrder: ['col1'],
509
+ enablePersistence: false
510
+ }));
511
+
512
+ expect(result.current.isLoaded).toBe(true);
513
+ });
514
+
515
+ it('returns isLoaded=true immediately when tableId is missing', () => {
516
+ const { result } = renderHook(() => useColumnOrderPersistence({
517
+ defaultOrder: ['col1'],
518
+ enablePersistence: true
519
+ }));
520
+
521
+ expect(result.current.isLoaded).toBe(true);
522
+ });
523
+ });
524
+ });
525
+