@jmruthers/pace-core 0.5.114 → 0.5.116

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 (236) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
  28. package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +43 -16
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +32 -17
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ImportModal.tsx +25 -2
  163. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  164. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  165. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  166. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  168. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  169. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  170. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  171. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  172. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  173. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  174. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  175. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  176. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  177. package/src/components/EventSelector/EventSelector.tsx +5 -25
  178. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  179. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  184. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  186. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  187. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  188. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  189. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  190. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  191. package/src/components/Select/Select.tsx +8 -0
  192. package/src/components/Toast/Toast.test.tsx +8 -7
  193. package/src/components/Toast/Toast.tsx +4 -4
  194. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  195. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  196. package/src/hooks/useEventTheme.ts +49 -18
  197. package/src/hooks/usePermissionCache.ts +5 -3
  198. package/src/hooks/useSecureDataAccess.ts +11 -1
  199. package/src/hooks/useToast.ts +11 -12
  200. package/src/providers/services/EventServiceProvider.tsx +15 -8
  201. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  202. package/src/rbac/audit.test.ts +206 -0
  203. package/src/rbac/audit.ts +37 -2
  204. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  205. package/src/rbac/errors.test.ts +340 -0
  206. package/src/rbac/hooks/index.ts +9 -0
  207. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  208. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  209. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  210. package/src/services/AuthService.ts +10 -0
  211. package/src/services/EventService.ts +111 -50
  212. package/src/services/__tests__/AuthService.test.ts +1 -1
  213. package/src/services/__tests__/EventService.test.ts +60 -45
  214. package/src/services/interfaces/IEventService.ts +1 -1
  215. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  216. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  217. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  218. package/src/utils/file-reference.test.ts +214 -0
  219. package/dist/chunk-3OGQLOJM.js.map +0 -1
  220. package/dist/chunk-4OX5PXHX.js.map +0 -1
  221. package/dist/chunk-5CDJCTOO.js +0 -190
  222. package/dist/chunk-5YIZFEUQ.js.map +0 -1
  223. package/dist/chunk-F6QB26OS.js.map +0 -1
  224. package/dist/chunk-KTHLNIMA.js.map +0 -1
  225. package/dist/chunk-OO3V7W4H.js.map +0 -1
  226. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  227. package/src/rbac/audit-enhanced.ts +0 -351
  228. /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  230. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  231. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  232. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  233. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  234. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  235. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  236. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -0,0 +1,451 @@
1
+ /**
2
+ * @file DataTable Error Boundary Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for DataTableErrorBoundary component following testing guidelines.
8
+ * Tests cover all major functionality, edge cases, and error handling.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, waitFor } from '@testing-library/react';
13
+ import { renderHook } from '@testing-library/react';
14
+ import userEvent from '@testing-library/user-event';
15
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
16
+ import { DataTableErrorBoundary, useDataTableErrorBoundary } from '../DataTableErrorBoundary';
17
+
18
+ // Mock logger
19
+ vi.mock('../../../utils/logger', () => ({
20
+ createLogger: () => ({
21
+ debug: vi.fn(),
22
+ info: vi.fn(),
23
+ warn: vi.fn(),
24
+ error: vi.fn(),
25
+ }),
26
+ }));
27
+
28
+ // Mock Alert components
29
+ vi.mock('../../Alert/Alert', () => ({
30
+ Alert: ({ children, variant, className }: any) => (
31
+ <div data-testid="alert" data-variant={variant} className={className}>{children}</div>
32
+ ),
33
+ AlertTitle: ({ children }: any) => <div data-testid="alert-title">{children}</div>,
34
+ AlertDescription: ({ children, className }: any) => (
35
+ <div data-testid="alert-description" className={className}>{children}</div>
36
+ ),
37
+ }));
38
+
39
+ // Mock Button component
40
+ vi.mock('../../Button/Button', () => ({
41
+ Button: ({ children, onClick, variant, size, className }: any) => (
42
+ <button
43
+ onClick={onClick}
44
+ data-variant={variant}
45
+ data-size={size}
46
+ className={className}
47
+ >
48
+ {children}
49
+ </button>
50
+ ),
51
+ }));
52
+
53
+ // Component that throws an error
54
+ const ThrowError = ({ shouldThrow, message = 'Test error' }: { shouldThrow: boolean; message?: string }) => {
55
+ if (shouldThrow) {
56
+ throw new Error(message);
57
+ }
58
+ return <div>No error</div>;
59
+ };
60
+
61
+ describe('[component] DataTableErrorBoundary', () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ // Suppress console.error for error boundary tests
65
+ vi.spyOn(console, 'error').mockImplementation(() => {});
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ vi.clearAllMocks();
71
+ });
72
+
73
+ describe('Rendering', () => {
74
+ it('renders children when no error occurs', () => {
75
+ render(
76
+ <DataTableErrorBoundary>
77
+ <div>Test content</div>
78
+ </DataTableErrorBoundary>
79
+ );
80
+
81
+ expect(screen.getByText('Test content')).toBeInTheDocument();
82
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
83
+ });
84
+
85
+ it('renders default error UI when error occurs', () => {
86
+ render(
87
+ <DataTableErrorBoundary>
88
+ <ThrowError shouldThrow={true} />
89
+ </DataTableErrorBoundary>
90
+ );
91
+
92
+ // Alert component uses role="alert", not data-testid
93
+ expect(screen.getByRole('alert')).toBeInTheDocument();
94
+ expect(screen.getByText('DataTable Error')).toBeInTheDocument();
95
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
96
+ });
97
+
98
+ it('renders custom fallback when provided', () => {
99
+ const customFallback = <div data-testid="custom-fallback">Custom error UI</div>;
100
+
101
+ render(
102
+ <DataTableErrorBoundary fallback={customFallback}>
103
+ <ThrowError shouldThrow={true} />
104
+ </DataTableErrorBoundary>
105
+ );
106
+
107
+ expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
108
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
109
+ });
110
+ });
111
+
112
+ describe('Error Display', () => {
113
+ it('displays error message when available', () => {
114
+ render(
115
+ <DataTableErrorBoundary>
116
+ <ThrowError shouldThrow={true} message="Custom error message" />
117
+ </DataTableErrorBoundary>
118
+ );
119
+
120
+ expect(screen.getByText('Custom error message')).toBeInTheDocument();
121
+ });
122
+
123
+ it('displays error details in collapsible section', () => {
124
+ render(
125
+ <DataTableErrorBoundary showErrorDetails={true}>
126
+ <ThrowError shouldThrow={true} message="Test error" />
127
+ </DataTableErrorBoundary>
128
+ );
129
+
130
+ const details = screen.getByText('Error Details');
131
+ expect(details).toBeInTheDocument();
132
+ });
133
+
134
+ it('hides error details when showErrorDetails is false', () => {
135
+ render(
136
+ <DataTableErrorBoundary showErrorDetails={false}>
137
+ <ThrowError shouldThrow={true} message="Test error" />
138
+ </DataTableErrorBoundary>
139
+ );
140
+
141
+ // Error details section is shown when error.message exists
142
+ // showErrorDetails only controls stack trace visibility, not the details section
143
+ // So we check that the details section exists but stack trace is not shown
144
+ expect(screen.getByText('Error Details')).toBeInTheDocument();
145
+ // Stack trace should not be visible when showErrorDetails is false
146
+ expect(screen.queryByText(/Stack Trace/i)).not.toBeInTheDocument();
147
+ });
148
+
149
+ it('displays stack trace when showErrorDetails is true', () => {
150
+ render(
151
+ <DataTableErrorBoundary showErrorDetails={true}>
152
+ <ThrowError shouldThrow={true} message="Test error" />
153
+ </DataTableErrorBoundary>
154
+ );
155
+
156
+ // Stack trace should be in the details section
157
+ expect(screen.getByText('Error Details')).toBeInTheDocument();
158
+ });
159
+ });
160
+
161
+ describe('Error Handling', () => {
162
+ it('calls onError callback when error occurs', () => {
163
+ const onError = vi.fn();
164
+
165
+ render(
166
+ <DataTableErrorBoundary onError={onError}>
167
+ <ThrowError shouldThrow={true} />
168
+ </DataTableErrorBoundary>
169
+ );
170
+
171
+ expect(onError).toHaveBeenCalledTimes(1);
172
+ expect(onError).toHaveBeenCalledWith(
173
+ expect.any(Error),
174
+ expect.objectContaining({
175
+ componentStack: expect.any(String),
176
+ })
177
+ );
178
+ });
179
+
180
+ it('does not call onError when no error occurs', () => {
181
+ const onError = vi.fn();
182
+
183
+ render(
184
+ <DataTableErrorBoundary onError={onError}>
185
+ <div>No error</div>
186
+ </DataTableErrorBoundary>
187
+ );
188
+
189
+ expect(onError).not.toHaveBeenCalled();
190
+ });
191
+ });
192
+
193
+ describe('Retry Functionality', () => {
194
+ it('renders retry button when showRetryButton is true', () => {
195
+ render(
196
+ <DataTableErrorBoundary showRetryButton={true}>
197
+ <ThrowError shouldThrow={true} />
198
+ </DataTableErrorBoundary>
199
+ );
200
+
201
+ const retryButton = screen.getByRole('button', { name: /retry/i });
202
+ expect(retryButton).toBeInTheDocument();
203
+ });
204
+
205
+ it('hides retry button when showRetryButton is false', () => {
206
+ render(
207
+ <DataTableErrorBoundary showRetryButton={false}>
208
+ <ThrowError shouldThrow={true} />
209
+ </DataTableErrorBoundary>
210
+ );
211
+
212
+ expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
213
+ });
214
+
215
+ it('calls onRetry callback when retry button is clicked', async () => {
216
+ const user = userEvent.setup();
217
+ const onRetry = vi.fn();
218
+
219
+ render(
220
+ <DataTableErrorBoundary onRetry={onRetry} showRetryButton={true}>
221
+ <ThrowError shouldThrow={true} />
222
+ </DataTableErrorBoundary>
223
+ );
224
+
225
+ const retryButton = screen.getByRole('button', { name: /retry/i });
226
+ await user.click(retryButton);
227
+
228
+ expect(onRetry).toHaveBeenCalledTimes(1);
229
+ });
230
+
231
+ it('resets error state when retry button is clicked', async () => {
232
+ const user = userEvent.setup();
233
+
234
+ const { rerender } = render(
235
+ <DataTableErrorBoundary showRetryButton={true}>
236
+ <ThrowError shouldThrow={true} />
237
+ </DataTableErrorBoundary>
238
+ );
239
+
240
+ expect(screen.getByRole('alert')).toBeInTheDocument();
241
+
242
+ const retryButton = screen.getByRole('button', { name: /retry/i });
243
+ await user.click(retryButton);
244
+
245
+ // After retry, error state is reset after a timeout (100ms)
246
+ // But the child still throws, so it will be caught again
247
+ // We need to wait for the retry to complete, then rerender with shouldThrow=false
248
+ await waitFor(() => {
249
+ // Wait for the timeout to complete
250
+ }, { timeout: 200 });
251
+
252
+ // Now rerender with shouldThrow=false to prevent re-throwing
253
+ rerender(
254
+ <DataTableErrorBoundary showRetryButton={true}>
255
+ <ThrowError shouldThrow={false} />
256
+ </DataTableErrorBoundary>
257
+ );
258
+
259
+ // Now the error should be gone since child doesn't throw
260
+ await waitFor(() => {
261
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
262
+ }, { timeout: 200 });
263
+ });
264
+
265
+ it('displays retry count in button text', () => {
266
+ render(
267
+ <DataTableErrorBoundary showRetryButton={true} maxRetries={3}>
268
+ <ThrowError shouldThrow={true} />
269
+ </DataTableErrorBoundary>
270
+ );
271
+
272
+ const retryButton = screen.getByRole('button', { name: /retry/i });
273
+ expect(retryButton).toHaveTextContent('1/3');
274
+ });
275
+
276
+ it('hides retry button when max retries reached', async () => {
277
+ const user = userEvent.setup();
278
+
279
+ render(
280
+ <DataTableErrorBoundary showRetryButton={true} maxRetries={1}>
281
+ <ThrowError shouldThrow={true} />
282
+ </DataTableErrorBoundary>
283
+ );
284
+
285
+ const retryButton = screen.getByRole('button', { name: /retry/i });
286
+ expect(retryButton).toHaveTextContent('1/1');
287
+
288
+ await user.click(retryButton);
289
+
290
+ // After clicking retry, state updates async - wait for the update
291
+ // The retry increments retryCount, so retryCount becomes 1, which equals maxRetries (1)
292
+ // So the button should be hidden after state update
293
+ await waitFor(() => {
294
+ expect(screen.queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
295
+ }, { timeout: 500 });
296
+ });
297
+ });
298
+
299
+ describe('Reset Functionality', () => {
300
+ it('renders reset button', () => {
301
+ render(
302
+ <DataTableErrorBoundary>
303
+ <ThrowError shouldThrow={true} />
304
+ </DataTableErrorBoundary>
305
+ );
306
+
307
+ const resetButton = screen.getByRole('button', { name: /reset/i });
308
+ expect(resetButton).toBeInTheDocument();
309
+ });
310
+
311
+ it('resets error state when reset button is clicked', async () => {
312
+ const user = userEvent.setup();
313
+
314
+ const { rerender } = render(
315
+ <DataTableErrorBoundary>
316
+ <ThrowError shouldThrow={true} />
317
+ </DataTableErrorBoundary>
318
+ );
319
+
320
+ expect(screen.getByRole('alert')).toBeInTheDocument();
321
+
322
+ const resetButton = screen.getByRole('button', { name: /reset/i });
323
+
324
+ // First, rerender with shouldThrow=false before clicking reset
325
+ // This prevents the error from being re-caught after reset
326
+ rerender(
327
+ <DataTableErrorBoundary>
328
+ <ThrowError shouldThrow={false} />
329
+ </DataTableErrorBoundary>
330
+ );
331
+
332
+ // Now click reset - the error state should be cleared
333
+ await user.click(resetButton);
334
+
335
+ // After reset, error state is cleared immediately (no timeout)
336
+ // Since child doesn't throw, the error should be gone
337
+ await waitFor(() => {
338
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
339
+ }, { timeout: 300 });
340
+ });
341
+ });
342
+
343
+ describe('Edge Cases', () => {
344
+ it('handles errors with no message', () => {
345
+ const ErrorWithoutMessage = () => {
346
+ throw new Error();
347
+ };
348
+
349
+ render(
350
+ <DataTableErrorBoundary>
351
+ <ErrorWithoutMessage />
352
+ </DataTableErrorBoundary>
353
+ );
354
+
355
+ expect(screen.getByRole('alert')).toBeInTheDocument();
356
+ expect(screen.getByText('An unexpected error occurred')).toBeInTheDocument();
357
+ });
358
+
359
+ it('handles multiple errors in sequence', async () => {
360
+ const user = userEvent.setup();
361
+
362
+ const { rerender } = render(
363
+ <DataTableErrorBoundary showRetryButton={true}>
364
+ <ThrowError shouldThrow={true} />
365
+ </DataTableErrorBoundary>
366
+ );
367
+
368
+ expect(screen.getByRole('alert')).toBeInTheDocument();
369
+
370
+ const retryButton = screen.getByRole('button', { name: /retry/i });
371
+ await user.click(retryButton);
372
+
373
+ // Trigger another error
374
+ rerender(
375
+ <DataTableErrorBoundary showRetryButton={true}>
376
+ <ThrowError shouldThrow={true} message="Second error" />
377
+ </DataTableErrorBoundary>
378
+ );
379
+
380
+ expect(screen.getByRole('alert')).toBeInTheDocument();
381
+ });
382
+
383
+ it('handles cleanup on unmount', () => {
384
+ const { unmount } = render(
385
+ <DataTableErrorBoundary>
386
+ <ThrowError shouldThrow={true} />
387
+ </DataTableErrorBoundary>
388
+ );
389
+
390
+ expect(() => {
391
+ unmount();
392
+ }).not.toThrow();
393
+ });
394
+ });
395
+
396
+ describe('Accessibility', () => {
397
+ it('provides accessible error message', () => {
398
+ render(
399
+ <DataTableErrorBoundary>
400
+ <ThrowError shouldThrow={true} message="Accessible error" />
401
+ </DataTableErrorBoundary>
402
+ );
403
+
404
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
405
+ expect(screen.getByText('Accessible error')).toBeInTheDocument();
406
+ });
407
+
408
+ it('provides accessible button labels', () => {
409
+ render(
410
+ <DataTableErrorBoundary showRetryButton={true}>
411
+ <ThrowError shouldThrow={true} />
412
+ </DataTableErrorBoundary>
413
+ );
414
+
415
+ const retryButton = screen.getByRole('button', { name: /retry/i });
416
+ const resetButton = screen.getByRole('button', { name: /reset/i });
417
+
418
+ expect(retryButton).toBeInTheDocument();
419
+ expect(resetButton).toBeInTheDocument();
420
+ });
421
+ });
422
+ });
423
+
424
+ describe('[hook] useDataTableErrorBoundary', () => {
425
+ it('initializes with no error', () => {
426
+ const { result } = renderHook(() => useDataTableErrorBoundary());
427
+
428
+ expect(result.current).toHaveProperty('captureError');
429
+ expect(result.current).toHaveProperty('resetError');
430
+ });
431
+
432
+ it('captures error when captureError is called', () => {
433
+ const { result } = renderHook(() => useDataTableErrorBoundary());
434
+
435
+ const testError = new Error('Test error');
436
+ result.current.captureError(testError);
437
+
438
+ // The hook should capture the error
439
+ expect(result.current).toBeDefined();
440
+ });
441
+
442
+ it('resets error when resetError is called', () => {
443
+ const { result } = renderHook(() => useDataTableErrorBoundary());
444
+
445
+ result.current.resetError();
446
+
447
+ // Should not throw
448
+ expect(result.current).toBeDefined();
449
+ });
450
+ });
451
+