@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,412 @@
1
+ /**
2
+ * @file View Row Modal Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for ViewRowModal component following testing guidelines.
8
+ * Tests cover all major functionality, edge cases, and user interactions.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, within } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { ViewRowModal } from '../ViewRowModal';
16
+
17
+ // Mock Dialog components - Use the same pattern as other tests
18
+ // Note: Path must match the import in ViewRowModal.tsx exactly
19
+ vi.mock('../../Dialog/Dialog', async () => {
20
+ const actual = await vi.importActual('../../Dialog/Dialog');
21
+ return {
22
+ ...actual,
23
+ Dialog: ({ children, open, onOpenChange }: any) => (
24
+ open ? <div role="dialog" data-testid="dialog">{children}</div> : null
25
+ ),
26
+ DialogContent: ({ children, className }: any) => (
27
+ <div data-testid="dialog-content" className={className}>{children}</div>
28
+ ),
29
+ DialogHeader: ({ children }: any) => (
30
+ <div data-testid="dialog-header">{children}</div>
31
+ ),
32
+ DialogTitle: ({ children, className }: any) => (
33
+ <h2 data-testid="dialog-title" className={className} role="heading" aria-level={2}>{children}</h2>
34
+ ),
35
+ };
36
+ });
37
+
38
+ // Mock Button component
39
+ vi.mock('../../Button/Button', () => ({
40
+ Button: ({ children, onClick, variant, size, className, 'aria-label': ariaLabel }: any) => (
41
+ <button
42
+ onClick={onClick}
43
+ data-variant={variant}
44
+ data-size={size}
45
+ className={className}
46
+ aria-label={ariaLabel}
47
+ >
48
+ {children}
49
+ </button>
50
+ ),
51
+ }));
52
+
53
+ // Mock lucide-react icons
54
+ vi.mock('lucide-react', () => ({
55
+ X: ({ className }: { className?: string }) => (
56
+ <span data-testid="x-icon" className={className}>X</span>
57
+ ),
58
+ }));
59
+
60
+ interface TestData extends Record<string, any> {
61
+ id: string;
62
+ name: string;
63
+ email: string;
64
+ age: number;
65
+ active: boolean;
66
+ createdAt: Date;
67
+ }
68
+
69
+ describe('[component] ViewRowModal', () => {
70
+ const mockData: TestData = {
71
+ id: '1',
72
+ name: 'John Doe',
73
+ email: 'john@example.com',
74
+ age: 30,
75
+ active: true,
76
+ createdAt: new Date('2024-01-01'),
77
+ };
78
+
79
+ const defaultProps = {
80
+ isOpen: true,
81
+ onClose: vi.fn(),
82
+ data: mockData,
83
+ };
84
+
85
+ beforeEach(() => {
86
+ vi.clearAllMocks();
87
+ });
88
+
89
+ afterEach(() => {
90
+ vi.clearAllMocks();
91
+ });
92
+
93
+ describe('Rendering', () => {
94
+ it('returns null when data is null', () => {
95
+ const { container } = render(
96
+ <ViewRowModal {...defaultProps} data={null} />
97
+ );
98
+ expect(container.firstChild).toBeNull();
99
+ });
100
+
101
+ it('returns null when modal is closed', () => {
102
+ const { container } = render(
103
+ <ViewRowModal {...defaultProps} isOpen={false} />
104
+ );
105
+ expect(container.firstChild).toBeNull();
106
+ });
107
+
108
+ it('renders modal when open with data', () => {
109
+ render(<ViewRowModal {...defaultProps} />);
110
+
111
+ // Dialog renders with role="dialog" from Radix UI
112
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
113
+ // DialogContent and DialogHeader are rendered but may not have testids in actual implementation
114
+ // Check for content instead
115
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
116
+ });
117
+
118
+ it('renders default title when title prop is not provided', () => {
119
+ render(<ViewRowModal {...defaultProps} />);
120
+
121
+ // DialogTitle renders as h2, check for text content
122
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
123
+ });
124
+
125
+ it('renders custom title when title prop is provided', () => {
126
+ render(<ViewRowModal {...defaultProps} title="Custom Title" />);
127
+
128
+ // DialogTitle renders as h2, check for text content
129
+ expect(screen.getByText('Custom Title')).toBeInTheDocument();
130
+ });
131
+
132
+ it('renders all data fields', () => {
133
+ render(<ViewRowModal {...defaultProps} />);
134
+
135
+ expect(screen.getByText(/id/i)).toBeInTheDocument();
136
+ expect(screen.getByText(/name/i)).toBeInTheDocument();
137
+ expect(screen.getByText(/email/i)).toBeInTheDocument();
138
+ expect(screen.getByText(/age/i)).toBeInTheDocument();
139
+ expect(screen.getByText(/active/i)).toBeInTheDocument();
140
+ });
141
+
142
+ it('renders field values correctly', () => {
143
+ render(<ViewRowModal {...defaultProps} />);
144
+
145
+ expect(screen.getByText('1')).toBeInTheDocument();
146
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
147
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
148
+ expect(screen.getByText('30')).toBeInTheDocument();
149
+ expect(screen.getByText('true')).toBeInTheDocument();
150
+ });
151
+
152
+ it('formats Date values as locale date strings', () => {
153
+ render(<ViewRowModal {...defaultProps} />);
154
+
155
+ const dateValue = screen.getByText(new Date('2024-01-01').toLocaleDateString());
156
+ expect(dateValue).toBeInTheDocument();
157
+ });
158
+
159
+ it('formats object values as JSON strings', () => {
160
+ const dataWithObject = {
161
+ ...mockData,
162
+ metadata: { key: 'value', nested: { data: 'test' } },
163
+ };
164
+
165
+ render(<ViewRowModal {...defaultProps} data={dataWithObject} />);
166
+
167
+ // JSON stringified objects contain the key parts
168
+ expect(screen.getByText(/key/i)).toBeInTheDocument();
169
+ expect(screen.getByText(/value/i)).toBeInTheDocument();
170
+ expect(screen.getByText(/nested/i)).toBeInTheDocument();
171
+ });
172
+
173
+ it('handles null values by displaying empty string', () => {
174
+ const dataWithNull = {
175
+ ...mockData,
176
+ nullableField: null,
177
+ };
178
+
179
+ render(<ViewRowModal {...defaultProps} data={dataWithNull} />);
180
+
181
+ // Check that the field label is rendered
182
+ expect(screen.getByText(/nullable field/i)).toBeInTheDocument();
183
+ });
184
+
185
+ it('capitalizes field names by replacing camelCase with spaces', () => {
186
+ const dataWithCamelCase = {
187
+ ...mockData,
188
+ firstName: 'John',
189
+ lastName: 'Doe',
190
+ };
191
+
192
+ render(<ViewRowModal {...defaultProps} data={dataWithCamelCase} />);
193
+
194
+ expect(screen.getByText(/first name/i)).toBeInTheDocument();
195
+ expect(screen.getByText(/last name/i)).toBeInTheDocument();
196
+ });
197
+ });
198
+
199
+ describe('User Interactions', () => {
200
+ it('calls onClose when close button is clicked', async () => {
201
+ const user = userEvent.setup();
202
+ const handleClose = vi.fn();
203
+
204
+ render(<ViewRowModal {...defaultProps} onClose={handleClose} />);
205
+
206
+ // Get the main "Close" button (not the X icon button)
207
+ const closeButtons = screen.getAllByRole('button', { name: /close/i });
208
+ const mainCloseButton = closeButtons.find(btn => btn.textContent === 'Close');
209
+ expect(mainCloseButton).toBeInTheDocument();
210
+ if (mainCloseButton) {
211
+ await user.click(mainCloseButton);
212
+ expect(handleClose).toHaveBeenCalledTimes(1);
213
+ }
214
+ });
215
+
216
+ it('calls onClose when X icon button is clicked', async () => {
217
+ const user = userEvent.setup();
218
+ const handleClose = vi.fn();
219
+
220
+ render(<ViewRowModal {...defaultProps} onClose={handleClose} />);
221
+
222
+ // Get the X icon button specifically (first button with X icon)
223
+ const xButtons = screen.getAllByTestId('x-icon');
224
+ const xButton = xButtons[0].closest('button');
225
+ expect(xButton).toBeInTheDocument();
226
+ if (xButton) {
227
+ await user.click(xButton);
228
+ expect(handleClose).toHaveBeenCalledTimes(1);
229
+ }
230
+ });
231
+
232
+ it('calls onOpenChange when dialog state changes', async () => {
233
+ const user = userEvent.setup();
234
+ const handleOpenChange = vi.fn();
235
+
236
+ render(
237
+ <ViewRowModal
238
+ {...defaultProps}
239
+ onClose={handleOpenChange}
240
+ />
241
+ );
242
+
243
+ // Get the main Close button (not the X icon button)
244
+ const closeButtons = screen.getAllByRole('button', { name: /close/i });
245
+ const mainCloseButton = closeButtons.find(btn => btn.textContent === 'Close');
246
+ expect(mainCloseButton).toBeInTheDocument();
247
+ if (mainCloseButton) {
248
+ await user.click(mainCloseButton);
249
+ expect(handleOpenChange).toHaveBeenCalled();
250
+ }
251
+ });
252
+ });
253
+
254
+ describe('Edge Cases', () => {
255
+ it('handles empty data object', () => {
256
+ render(<ViewRowModal {...defaultProps} data={{}} />);
257
+
258
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
259
+ expect(screen.getAllByRole('button', { name: /close/i }).length).toBeGreaterThan(0);
260
+ });
261
+
262
+ it('handles data with undefined values', () => {
263
+ const dataWithUndefined = {
264
+ ...mockData,
265
+ optionalField: undefined,
266
+ };
267
+
268
+ render(<ViewRowModal {...defaultProps} data={dataWithUndefined} />);
269
+
270
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
271
+ });
272
+
273
+ it('handles data with array values', () => {
274
+ const dataWithArray = {
275
+ ...mockData,
276
+ tags: ['tag1', 'tag2', 'tag3'],
277
+ };
278
+
279
+ render(<ViewRowModal {...defaultProps} data={dataWithArray} />);
280
+
281
+ // Check that array values are displayed (they'll be JSON stringified)
282
+ expect(screen.getByText(/tag1/i)).toBeInTheDocument();
283
+ expect(screen.getByText(/tag2/i)).toBeInTheDocument();
284
+ expect(screen.getByText(/tag3/i)).toBeInTheDocument();
285
+ });
286
+
287
+ it('handles data with nested objects', () => {
288
+ const dataWithNested = {
289
+ ...mockData,
290
+ address: {
291
+ street: '123 Main St',
292
+ city: 'New York',
293
+ zip: '10001',
294
+ },
295
+ };
296
+
297
+ render(<ViewRowModal {...defaultProps} data={dataWithNested} />);
298
+
299
+ // JSON stringified objects contain the key parts
300
+ expect(screen.getByText(/street/i)).toBeInTheDocument();
301
+ expect(screen.getByText(/123 Main St/i)).toBeInTheDocument();
302
+ expect(screen.getByText(/New York/i)).toBeInTheDocument();
303
+ });
304
+
305
+ it('handles very long string values', () => {
306
+ const longString = 'a'.repeat(1000);
307
+ const dataWithLongString = {
308
+ ...mockData,
309
+ description: longString,
310
+ };
311
+
312
+ render(<ViewRowModal {...defaultProps} data={dataWithLongString} />);
313
+
314
+ expect(screen.getByText(longString)).toBeInTheDocument();
315
+ });
316
+
317
+ it('handles special characters in field names', () => {
318
+ const dataWithSpecial = {
319
+ ...mockData,
320
+ 'field-with-special-chars!@#': 'value',
321
+ };
322
+
323
+ render(<ViewRowModal {...defaultProps} data={dataWithSpecial} />);
324
+
325
+ // The component only handles camelCase, not special characters
326
+ // Special characters remain in the field name, so check for parts of the name
327
+ expect(screen.getByText(/field/i)).toBeInTheDocument();
328
+ expect(screen.getByText('value')).toBeInTheDocument();
329
+ });
330
+
331
+ it('handles numeric zero values', () => {
332
+ const dataWithZero = {
333
+ ...mockData,
334
+ count: 0,
335
+ };
336
+
337
+ render(<ViewRowModal {...defaultProps} data={dataWithZero} />);
338
+
339
+ // Zero should render as "0" - check for it in the value cell
340
+ const countLabel = screen.getByText(/count:/i);
341
+ const countValueCell = countLabel.closest('div')?.nextElementSibling;
342
+ expect(countValueCell).toHaveTextContent('0');
343
+ });
344
+
345
+ it('handles boolean false values', () => {
346
+ const dataWithFalse = {
347
+ ...mockData,
348
+ active: false,
349
+ };
350
+
351
+ render(<ViewRowModal {...defaultProps} data={dataWithFalse} />);
352
+
353
+ // Boolean false should render as "false" - check for it in the value cell
354
+ const activeLabel = screen.getByText(/active:/i);
355
+ const activeValueCell = activeLabel.closest('div')?.nextElementSibling;
356
+ expect(activeValueCell).toHaveTextContent('false');
357
+ });
358
+ });
359
+
360
+ describe('Accessibility', () => {
361
+ it('provides close button with aria-label', () => {
362
+ render(<ViewRowModal {...defaultProps} />);
363
+
364
+ const buttons = screen.getAllByRole('button');
365
+ expect(buttons.length).toBeGreaterThan(0);
366
+ });
367
+
368
+ it('provides main close button with accessible text', () => {
369
+ render(<ViewRowModal {...defaultProps} />);
370
+
371
+ const closeButtons = screen.getAllByRole('button', { name: /close/i });
372
+ expect(closeButtons.length).toBeGreaterThan(0);
373
+ });
374
+
375
+ it('renders dialog with proper structure', () => {
376
+ render(<ViewRowModal {...defaultProps} />);
377
+
378
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
379
+ // Check for content structure instead of testids
380
+ expect(screen.getByText('Row Details')).toBeInTheDocument();
381
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
382
+ });
383
+ });
384
+
385
+ describe('Layout and Styling', () => {
386
+ it('applies max-width and max-height classes to dialog content', () => {
387
+ render(<ViewRowModal {...defaultProps} />);
388
+
389
+ // DialogContent receives className prop, check if dialog has the classes
390
+ const dialog = screen.getByRole('dialog');
391
+ // The classes are applied to DialogContent which is inside the dialog
392
+ // Check that dialog is rendered (classes are on the inner element)
393
+ expect(dialog).toBeInTheDocument();
394
+ });
395
+
396
+ it('renders field labels with proper styling classes', () => {
397
+ render(<ViewRowModal {...defaultProps} />);
398
+
399
+ // Check that field labels are rendered (they should have specific classes)
400
+ const nameLabel = screen.getByText(/name/i);
401
+ expect(nameLabel).toBeInTheDocument();
402
+ });
403
+
404
+ it('renders close button in header with proper styling', () => {
405
+ render(<ViewRowModal {...defaultProps} />);
406
+
407
+ const xIcons = screen.getAllByTestId('x-icon');
408
+ expect(xIcons.length).toBeGreaterThan(0);
409
+ });
410
+ });
411
+ });
412
+
@@ -107,10 +107,14 @@ export function useTableHandlers<TData extends DataRecord>({
107
107
  : updaterOrValue as Record<string, boolean>;
108
108
 
109
109
  if (selection === undefined) {
110
+ // Uncontrolled mode - update internal state
110
111
  actions.setRowSelection(nextSelection);
112
+ // Still invoke callback so parent components can react (e.g., enable bulk delete button)
113
+ onRowSelectionChange?.(nextSelection);
111
114
  return;
112
115
  }
113
116
 
117
+ // Controlled mode - update via callback
114
118
  onRowSelectionChange?.(nextSelection);
115
119
  }, [actions, selection, state.rowSelection, onRowSelectionChange]);
116
120
 
@@ -216,34 +216,14 @@ export function EventSelector({
216
216
 
217
217
  // Default to the next upcoming event if none selected, fallback to most recent past event
218
218
  // IMPORTANT: Only auto-select if there's no persisted event being restored
219
+ // EventService handles all persistence internally - we don't check storage directly
219
220
  useEffect(() => {
220
221
  // Only auto-select if events are loaded, no event is selected, and not currently loading
222
+ // EventService will have already loaded persisted event if one exists
221
223
  if (!selectedEvent && events.length > 0 && !isLoading) {
222
- // Check if there's a persisted event that should be loaded
223
- // If persisted event exists in storage, don't auto-select - let it load first
224
- const persistedEventId = localStorage.getItem('pace-core-selected-event') || sessionStorage.getItem('pace-core-selected-event');
225
-
226
- if (persistedEventId) {
227
- // Check if the persisted event is in the available events list
228
- const persistedEvent = events.find(e => e.event_id === persistedEventId);
229
-
230
- if (persistedEvent) {
231
- // Persisted event exists in available events - it should have been loaded by EventService
232
- // If it's still not selected, there might be an issue, but don't auto-select as a fallback
233
- // The EventService should have handled this during initialization
234
- console.debug('[EventSelector] Persisted event found in storage but not selected:', persistedEventId);
235
- return;
236
- } else {
237
- // Persisted event ID exists but event is not in available events list
238
- // This means the user no longer has access to that event - clear it and auto-select
239
- localStorage.removeItem('pace-core-selected-event');
240
- sessionStorage.removeItem('pace-core-selected-event');
241
- autoSelectEvent();
242
- }
243
- } else {
244
- // No persisted event - safe to auto-select (new user or first visit)
245
- autoSelectEvent();
246
- }
224
+ // No event selected - safe to auto-select
225
+ // EventService has already checked for persisted events during initialization
226
+ autoSelectEvent();
247
227
  }
248
228
 
249
229
  function autoSelectEvent() {
@@ -87,8 +87,8 @@ vi.mock('../../hooks/useOrganisations', () => ({
87
87
  })),
88
88
  }));
89
89
 
90
- // Mock useEvents hook (optional - wrapped in try/catch in component)
91
- vi.mock('../../providers/EventsProvider', () => ({
90
+ // Mock useEvents hook (used by useEventTheme)
91
+ vi.mock('../../hooks/useEvents', () => ({
92
92
  useEvents: vi.fn(() => ({
93
93
  selectedEvent: { event_id: 'event-123' },
94
94
  events: [],
@@ -97,6 +97,11 @@ vi.mock('../../providers/EventsProvider', () => ({
97
97
  })),
98
98
  }));
99
99
 
100
+ // Mock useEventTheme to avoid EventServiceProvider requirement
101
+ vi.mock('../../hooks/useEventTheme', () => ({
102
+ useEventTheme: vi.fn(),
103
+ }));
104
+
100
105
  // Mock RBAC functions
101
106
  const mockIsPermitted = vi.fn().mockResolvedValue(true);
102
107
  const mockIsPermittedCached = vi.fn().mockResolvedValue(true);
@@ -603,7 +608,7 @@ describe('PaceAppLayout Component', () => {
603
608
  expect(screen.getByTestId('nav-item-home')).toBeInTheDocument();
604
609
  expect(screen.getByTestId('nav-item-dashboard')).toBeInTheDocument();
605
610
  expect(screen.getByTestId('nav-item-settings')).toBeInTheDocument();
606
- }, { timeout: 5000 });
611
+ }, { timeout: 2000 });
607
612
  });
608
613
 
609
614
  it('shows all navigation items when filterNavigationByPermissions is false', () => {
@@ -654,8 +659,8 @@ describe('PaceAppLayout Component', () => {
654
659
  'dashboard-page',
655
660
  true
656
661
  );
657
- }, { timeout: 5000 });
658
- }, { timeout: 6000 });
662
+ }, { timeout: 2000 });
663
+ }, { timeout: 3000 });
659
664
 
660
665
  it('uses default permission when route not in routePermissions', async () => {
661
666
  renderWithProviders(
@@ -681,8 +686,8 @@ describe('PaceAppLayout Component', () => {
681
686
  'dashboard',
682
687
  true
683
688
  );
684
- }, { timeout: 5000 });
685
- }, { timeout: 6000 });
689
+ }, { timeout: 2000 });
690
+ }, { timeout: 3000 });
686
691
  });
687
692
 
688
693
  describe('Super Admin Bypass', () => {
@@ -102,6 +102,7 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
102
102
  import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
103
103
  import { useOrganisations } from '../../hooks/useOrganisations';
104
104
  import { useEvents } from '../../hooks/useEvents';
105
+ import { useEventTheme } from '../../hooks/useEventTheme';
105
106
  import { useCan, useResolvedScope } from '../../rbac/hooks';
106
107
  import { createScopeFromEvent } from '../../rbac/utils/eventContext';
107
108
  import { getCurrentAppName } from '../../utils/appNameResolver';
@@ -359,6 +360,9 @@ export function PaceAppLayout({
359
360
  const navigate = useNavigate();
360
361
  const location = useLocation();
361
362
 
363
+ // Apply event theme colors automatically
364
+ useEventTheme();
365
+
362
366
  // Get selected event (optional)
363
367
  let selectedEvent: { event_id: string } | null = null;
364
368
  try {
@@ -74,8 +74,8 @@ vi.mock('../../../hooks/useOrganisations', () => ({
74
74
  useOrganisations: () => mockOrganisationContext
75
75
  }));
76
76
 
77
- // Mock useEvents hook (optional - wrapped in try/catch in component)
78
- vi.mock('../../../providers/EventsProvider', () => ({
77
+ // Mock useEvents hook (used by useEventTheme)
78
+ vi.mock('../../../hooks/useEvents', () => ({
79
79
  useEvents: vi.fn(() => ({
80
80
  selectedEvent: { event_id: 'event-123' },
81
81
  events: [],
@@ -84,6 +84,11 @@ vi.mock('../../../providers/EventsProvider', () => ({
84
84
  })),
85
85
  }));
86
86
 
87
+ // Mock useEventTheme to avoid EventServiceProvider requirement
88
+ vi.mock('../../../hooks/useEventTheme', () => ({
89
+ useEventTheme: vi.fn(),
90
+ }));
91
+
87
92
  // Mock usePermissionCache
88
93
  vi.mock('../../../hooks/usePermissionCache', () => ({
89
94
  usePermissionCache: () => ({
@@ -82,8 +82,8 @@ vi.mock('../../../hooks/useOrganisations', () => ({
82
82
  useOrganisations: () => mockOrganisationContext
83
83
  }));
84
84
 
85
- // Mock useEvents hook (optional - wrapped in try/catch in component)
86
- vi.mock('../../../providers/EventsProvider', () => ({
85
+ // Mock useEvents hook (used by useEventTheme)
86
+ vi.mock('../../../hooks/useEvents', () => ({
87
87
  useEvents: vi.fn(() => ({
88
88
  selectedEvent: { event_id: 'event-123' },
89
89
  events: [],
@@ -92,6 +92,11 @@ vi.mock('../../../providers/EventsProvider', () => ({
92
92
  })),
93
93
  }));
94
94
 
95
+ // Mock useEventTheme to avoid EventServiceProvider requirement
96
+ vi.mock('../../../hooks/useEventTheme', () => ({
97
+ useEventTheme: vi.fn(),
98
+ }));
99
+
95
100
  // Mock the new RBAC system
96
101
  vi.mock('../../../rbac/api', () => ({
97
102
  isPermitted: vi.fn().mockImplementation((input) => {
@@ -469,7 +474,7 @@ describe('PaceAppLayout Integration', () => {
469
474
 
470
475
  await waitFor(() => {
471
476
  expect(mockSignOut).toHaveBeenCalledTimes(1);
472
- }, { timeout: 5000 });
477
+ }, { timeout: 2000 });
473
478
  });
474
479
 
475
480
  it('handles password change flow correctly', async () => {
@@ -500,13 +505,13 @@ describe('PaceAppLayout Integration', () => {
500
505
  fireEvent.click(screen.getByTestId('sign-out-button'));
501
506
  await waitFor(() => {
502
507
  expect(mockSignOut).toHaveBeenCalled();
503
- }, { timeout: 5000 });
508
+ }, { timeout: 2000 });
504
509
 
505
510
  // Test password change error
506
511
  fireEvent.click(screen.getByTestId('change-password-button'));
507
512
  await waitFor(() => {
508
513
  expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
509
- }, { timeout: 5000 });
514
+ }, { timeout: 2000 });
510
515
  });
511
516
  });
512
517
 
@@ -698,7 +703,7 @@ describe('PaceAppLayout Integration', () => {
698
703
  fireEvent.click(screen.getByTestId('change-password-button'));
699
704
  await waitFor(() => {
700
705
  expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
701
- }, { timeout: 5000 });
706
+ }, { timeout: 2000 });
702
707
 
703
708
  // 3. User navigates back to home
704
709
  fireEvent.click(screen.getByTestId('nav-home'));
@@ -708,7 +713,7 @@ describe('PaceAppLayout Integration', () => {
708
713
  fireEvent.click(screen.getByTestId('sign-out-button'));
709
714
  await waitFor(() => {
710
715
  expect(mockSignOut).toHaveBeenCalledTimes(1);
711
- }, { timeout: 5000 });
716
+ }, { timeout: 2000 });
712
717
  });
713
718
 
714
719
  it('handles rapid navigation and interactions', async () => {
@@ -820,7 +825,7 @@ describe('PaceAppLayout Integration', () => {
820
825
  // Should work normally after recovery
821
826
  await waitFor(() => {
822
827
  expect(screen.getByTestId('mock-header')).toBeInTheDocument();
823
- }, { timeout: 5000 });
828
+ }, { timeout: 2000 });
824
829
  });
825
830
  });
826
831