@jmruthers/pace-core 0.5.115 → 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 (234) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-H5KJCAIS.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-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
  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-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
  28. package/dist/{chunk-OUU3SP6I.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-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
  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 +41 -14
  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 +29 -2
  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/ViewRowModal.tsx +1 -1
  163. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  164. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  165. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  166. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  167. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  168. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  169. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  170. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  171. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  172. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  173. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  174. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  175. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  176. package/src/components/EventSelector/EventSelector.tsx +5 -25
  177. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  178. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  179. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  184. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  185. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  186. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  187. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  188. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  189. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  190. package/src/components/Select/Select.tsx +8 -0
  191. package/src/components/Toast/Toast.tsx +1 -1
  192. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  193. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  194. package/src/hooks/useEventTheme.ts +49 -18
  195. package/src/hooks/usePermissionCache.ts +5 -3
  196. package/src/hooks/useSecureDataAccess.ts +11 -1
  197. package/src/hooks/useToast.ts +1 -1
  198. package/src/providers/services/EventServiceProvider.tsx +15 -8
  199. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  200. package/src/rbac/audit.test.ts +206 -0
  201. package/src/rbac/audit.ts +37 -2
  202. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  203. package/src/rbac/errors.test.ts +340 -0
  204. package/src/rbac/hooks/index.ts +9 -0
  205. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  206. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  207. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  208. package/src/services/AuthService.ts +10 -0
  209. package/src/services/EventService.ts +111 -50
  210. package/src/services/__tests__/AuthService.test.ts +1 -1
  211. package/src/services/__tests__/EventService.test.ts +60 -45
  212. package/src/services/interfaces/IEventService.ts +1 -1
  213. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  214. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  215. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  216. package/src/utils/file-reference.test.ts +214 -0
  217. package/dist/chunk-3OGQLOJM.js.map +0 -1
  218. package/dist/chunk-5CDJCTOO.js +0 -190
  219. package/dist/chunk-F6QB26OS.js.map +0 -1
  220. package/dist/chunk-KTHLNIMA.js.map +0 -1
  221. package/dist/chunk-OO3V7W4H.js.map +0 -1
  222. package/dist/chunk-SYXOZQ4P.js.map +0 -1
  223. package/dist/chunk-XYRZV7R5.js.map +0 -1
  224. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  225. package/src/rbac/audit-enhanced.ts +0 -351
  226. /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  227. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  228. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  229. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  230. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  231. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  232. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  233. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  234. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -14,22 +14,14 @@ import {
14
14
  DefaultPublicErrorFallback
15
15
  } from '../PublicErrorBoundary';
16
16
 
17
- // Store original console.error
18
- const originalError = console.error;
19
- let mockConsoleError: ReturnType<typeof vi.fn>;
17
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
20
18
 
21
19
  beforeEach(() => {
22
- // Create a fresh mock for each test
23
- mockConsoleError = vi.fn();
24
- vi.spyOn(console, 'error').mockImplementation((...args) => {
25
- mockConsoleError(...args);
26
- // Call original to allow React's error boundary to work
27
- originalError(...args);
28
- });
20
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
29
21
  });
30
22
 
31
23
  afterEach(() => {
32
- vi.restoreAllMocks();
24
+ consoleErrorSpy.mockRestore();
33
25
  });
34
26
 
35
27
  // Component that throws an error
@@ -80,7 +72,7 @@ describe('[component] PublicErrorBoundary', () => {
80
72
  );
81
73
 
82
74
  // Check that console.error was called (error boundary should trigger logging)
83
- expect(mockConsoleError).toHaveBeenCalled();
75
+ expect(consoleErrorSpy).toHaveBeenCalled();
84
76
  // Just verify an error was logged, don't check for exact message
85
77
  // as the development mode check might not be active in tests
86
78
  });
@@ -402,8 +402,16 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
402
402
  (typeof child === 'object' && 'type' in child && typeof child.type === 'function' && child.type.name === 'ChevronDown'))
403
403
  );
404
404
 
405
+ // Merge child's className with triggerProps className
406
+ const childClassName = (children as React.ReactElement).props.className;
407
+ const mergedClassName = cn(
408
+ triggerProps.className,
409
+ childClassName
410
+ );
411
+
405
412
  return React.cloneElement(children as React.ReactElement, {
406
413
  ...triggerProps,
414
+ className: mergedClassName,
407
415
  children: hasChevron ? childChildren : [
408
416
  ...childChildren,
409
417
  <ChevronDown
@@ -299,7 +299,7 @@ export function Toaster() {
299
299
  const { id, title, description, action, dismiss, duration, ...toastProps } = toast;
300
300
 
301
301
  return (
302
- <Toast key={id} {...toastProps}>
302
+ <Toast key={id} {...toastProps} duration={duration}>
303
303
  {title && <ToastTitle>{title}</ToastTitle>}
304
304
  {description && <ToastDescription>{description}</ToastDescription>}
305
305
  {action && action}
@@ -269,6 +269,311 @@ describe('usePublicEvent - Simple Tests', () => {
269
269
  });
270
270
  });
271
271
 
272
+ describe('Refetch Functionality', () => {
273
+ it('refetches data when refetch is called', async () => {
274
+ const mockEventData = {
275
+ event_id: '123',
276
+ event_name: 'Test Event',
277
+ event_date: '2024-01-01',
278
+ event_venue: 'Test Venue',
279
+ event_participants: 100,
280
+ event_colours: { primary: '#000000' },
281
+ organisation_id: 'org-123',
282
+ event_days: 1,
283
+ event_typicalunit: 'km',
284
+ event_rounddown: false,
285
+ event_youthmultiplier: 1.0,
286
+ event_catering_email: 'test@example.com',
287
+ event_news: 'Test news',
288
+ event_billing: 'Test billing',
289
+ event_footer: 'Test footer',
290
+ event_email: 'event@example.com',
291
+ event_logo: null
292
+ };
293
+
294
+ mockSupabaseClient.rpc.mockResolvedValue({
295
+ data: [mockEventData],
296
+ error: null
297
+ });
298
+
299
+ const { result } = renderHook(() => usePublicEvent('test-event', { enableCache: true }));
300
+
301
+ await waitFor(() => {
302
+ expect(result.current.isLoading).toBe(false);
303
+ });
304
+
305
+ const firstCallCount = mockSupabaseClient.rpc.mock.calls.length;
306
+
307
+ // Update mock to return different data
308
+ const updatedEventData = { ...mockEventData, event_name: 'Updated Event' };
309
+ mockSupabaseClient.rpc.mockResolvedValue({
310
+ data: [updatedEventData],
311
+ error: null
312
+ });
313
+
314
+ await result.current.refetch();
315
+
316
+ await waitFor(() => {
317
+ expect(result.current.event?.event_name).toBe('Updated Event');
318
+ }, { timeout: 2000 });
319
+
320
+ // Should have made another call
321
+ expect(mockSupabaseClient.rpc.mock.calls.length).toBeGreaterThan(firstCallCount);
322
+ });
323
+
324
+ it('clears cache before refetching', async () => {
325
+ const mockEventData = {
326
+ event_id: '123',
327
+ event_name: 'Test Event',
328
+ event_date: '2024-01-01',
329
+ event_venue: 'Test Venue',
330
+ event_participants: 100,
331
+ event_colours: { primary: '#000000' },
332
+ organisation_id: 'org-123',
333
+ event_days: 1,
334
+ event_typicalunit: 'km',
335
+ event_rounddown: false,
336
+ event_youthmultiplier: 1.0,
337
+ event_catering_email: 'test@example.com',
338
+ event_news: 'Test news',
339
+ event_billing: 'Test billing',
340
+ event_footer: 'Test footer',
341
+ event_email: 'event@example.com',
342
+ event_logo: null
343
+ };
344
+
345
+ mockSupabaseClient.rpc.mockResolvedValue({
346
+ data: [mockEventData],
347
+ error: null
348
+ });
349
+
350
+ const { result } = renderHook(() => usePublicEvent('test-event', { enableCache: true }));
351
+
352
+ await waitFor(() => {
353
+ expect(result.current.isLoading).toBe(false);
354
+ });
355
+
356
+ // Verify cache exists
357
+ const statsBefore = getPublicEventCacheStats();
358
+ expect(statsBefore.size).toBeGreaterThan(0);
359
+
360
+ await result.current.refetch();
361
+
362
+ // Cache should be cleared and then repopulated
363
+ await waitFor(() => {
364
+ expect(result.current.event).toBeDefined();
365
+ }, { timeout: 2000 });
366
+ });
367
+ });
368
+
369
+ describe('Caching Options', () => {
370
+ it('respects cacheTtl option', async () => {
371
+ const mockEventData = {
372
+ event_id: '123',
373
+ event_name: 'Test Event',
374
+ event_date: '2024-01-01',
375
+ event_venue: 'Test Venue',
376
+ event_participants: 100,
377
+ event_colours: { primary: '#000000' },
378
+ organisation_id: 'org-123',
379
+ event_days: 1,
380
+ event_typicalunit: 'km',
381
+ event_rounddown: false,
382
+ event_youthmultiplier: 1.0,
383
+ event_catering_email: 'test@example.com',
384
+ event_news: 'Test news',
385
+ event_billing: 'Test billing',
386
+ event_footer: 'Test footer',
387
+ event_email: 'event@example.com',
388
+ event_logo: null
389
+ };
390
+
391
+ mockSupabaseClient.rpc.mockResolvedValue({
392
+ data: [mockEventData],
393
+ error: null
394
+ });
395
+
396
+ const { result } = renderHook(() => usePublicEvent('test-event', {
397
+ enableCache: true,
398
+ cacheTtl: 1000 // 1 second
399
+ }));
400
+
401
+ await waitFor(() => {
402
+ expect(result.current.isLoading).toBe(false);
403
+ });
404
+
405
+ expect(result.current.event).toBeTruthy();
406
+ });
407
+
408
+ it('disables caching when enableCache is false', async () => {
409
+ const mockEventData = {
410
+ event_id: '123',
411
+ event_name: 'Test Event',
412
+ event_date: '2024-01-01',
413
+ event_venue: 'Test Venue',
414
+ event_participants: 100,
415
+ event_colours: { primary: '#000000' },
416
+ organisation_id: 'org-123',
417
+ event_days: 1,
418
+ event_typicalunit: 'km',
419
+ event_rounddown: false,
420
+ event_youthmultiplier: 1.0,
421
+ event_catering_email: 'test@example.com',
422
+ event_news: 'Test news',
423
+ event_billing: 'Test billing',
424
+ event_footer: 'Test footer',
425
+ event_email: 'event@example.com',
426
+ event_logo: null
427
+ };
428
+
429
+ mockSupabaseClient.rpc.mockResolvedValue({
430
+ data: [mockEventData],
431
+ error: null
432
+ });
433
+
434
+ const { result, rerender } = renderHook(() => usePublicEvent('test-event', {
435
+ enableCache: false
436
+ }));
437
+
438
+ await waitFor(() => {
439
+ expect(result.current.isLoading).toBe(false);
440
+ });
441
+
442
+ const firstCallCount = mockSupabaseClient.rpc.mock.calls.length;
443
+
444
+ // Rerender - should make another call since caching is disabled
445
+ rerender();
446
+
447
+ await waitFor(() => {
448
+ expect(result.current.event).toBeTruthy();
449
+ }, { timeout: 2000 });
450
+ });
451
+ });
452
+
453
+ describe('Error Handling', () => {
454
+ it('handles RPC errors that are not schema cache issues', async () => {
455
+ const rpcError = { message: 'Permission denied', code: 'PGRST301' };
456
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
457
+ data: null,
458
+ error: rpcError
459
+ });
460
+
461
+ const { result } = renderHook(() => usePublicEvent('test-event'));
462
+
463
+ await waitFor(() => {
464
+ expect(result.current.isLoading).toBe(false);
465
+ }, { timeout: 2000 });
466
+
467
+ expect(result.current.error).toBeInstanceOf(Error);
468
+ expect(result.current.error?.message).toBe('Permission denied');
469
+ expect(result.current.event).toBe(null);
470
+ });
471
+
472
+ it('handles exceptions during event fetch', async () => {
473
+ const error = new Error('Network timeout');
474
+ // Mock RPC to reject, and table access to also fail
475
+ mockSupabaseClient.rpc.mockRejectedValue(error);
476
+ mockSupabaseClient.from.mockReturnValue({
477
+ select: vi.fn().mockReturnThis(),
478
+ eq: vi.fn().mockReturnThis(),
479
+ not: vi.fn().mockReturnThis(),
480
+ limit: vi.fn().mockReturnThis(),
481
+ single: vi.fn().mockRejectedValue(error)
482
+ });
483
+
484
+ const { result } = renderHook(() => usePublicEvent('test-event'));
485
+
486
+ await waitFor(() => {
487
+ expect(result.current.isLoading).toBe(false);
488
+ }, { timeout: 2000 });
489
+
490
+ expect(result.current.error).toBeInstanceOf(Error);
491
+ expect(result.current.error?.message).toBe('Network timeout');
492
+ expect(result.current.event).toBe(null);
493
+ });
494
+
495
+ it('handles non-Error exceptions', async () => {
496
+ const stringError = 'String error';
497
+ // Mock RPC to reject, and table access to also fail
498
+ mockSupabaseClient.rpc.mockRejectedValue(stringError);
499
+ mockSupabaseClient.from.mockReturnValue({
500
+ select: vi.fn().mockReturnThis(),
501
+ eq: vi.fn().mockReturnThis(),
502
+ not: vi.fn().mockReturnThis(),
503
+ limit: vi.fn().mockReturnThis(),
504
+ single: vi.fn().mockRejectedValue(stringError)
505
+ });
506
+
507
+ const { result } = renderHook(() => usePublicEvent('test-event'));
508
+
509
+ await waitFor(() => {
510
+ expect(result.current.isLoading).toBe(false);
511
+ }, { timeout: 2000 });
512
+
513
+ expect(result.current.error).toBeInstanceOf(Error);
514
+ expect(result.current.error?.message).toBe('Unknown error occurred');
515
+ });
516
+ });
517
+
518
+ describe('Parameter Changes', () => {
519
+ it('refetches when eventCode changes', async () => {
520
+ const mockEventData1 = {
521
+ event_id: '123',
522
+ event_name: 'Event 1',
523
+ event_date: '2024-01-01',
524
+ event_venue: 'Venue 1',
525
+ event_participants: 100,
526
+ event_colours: { primary: '#000000' },
527
+ organisation_id: 'org-123',
528
+ event_days: 1,
529
+ event_typicalunit: 'km',
530
+ event_rounddown: false,
531
+ event_youthmultiplier: 1.0,
532
+ event_catering_email: 'test@example.com',
533
+ event_news: 'Test news',
534
+ event_billing: 'Test billing',
535
+ event_footer: 'Test footer',
536
+ event_email: 'event@example.com',
537
+ event_logo: null
538
+ };
539
+
540
+ const mockEventData2 = {
541
+ ...mockEventData1,
542
+ event_id: '456',
543
+ event_name: 'Event 2'
544
+ };
545
+
546
+ mockSupabaseClient.rpc
547
+ .mockResolvedValueOnce({
548
+ data: [mockEventData1],
549
+ error: null
550
+ })
551
+ .mockResolvedValueOnce({
552
+ data: [mockEventData2],
553
+ error: null
554
+ });
555
+
556
+ const { result, rerender } = renderHook(
557
+ ({ eventCode }) => usePublicEvent(eventCode),
558
+ { initialProps: { eventCode: 'event-1' } }
559
+ );
560
+
561
+ await waitFor(() => {
562
+ expect(result.current.isLoading).toBe(false);
563
+ }, { timeout: 2000 });
564
+
565
+ expect(result.current.event?.event_name).toBe('Event 1');
566
+
567
+ rerender({ eventCode: 'event-2' });
568
+
569
+ await waitFor(() => {
570
+ expect(result.current.isLoading).toBe(false);
571
+ }, { timeout: 2000 });
572
+
573
+ expect(result.current.event?.event_name).toBe('Event 2');
574
+ });
575
+ });
576
+
272
577
  describe('Edge Cases', () => {
273
578
  it('should handle null event data from RPC', async () => {
274
579
  mockSupabaseClient.rpc.mockResolvedValueOnce({
@@ -280,7 +585,7 @@ describe('usePublicEvent - Simple Tests', () => {
280
585
 
281
586
  await waitFor(() => {
282
587
  expect(result.current.isLoading).toBe(false);
283
- });
588
+ }, { timeout: 2000 });
284
589
 
285
590
  expect(result.current.event).toBe(null);
286
591
  expect(result.current.error).toEqual(new Error('Event not found'));
@@ -296,7 +601,7 @@ describe('usePublicEvent - Simple Tests', () => {
296
601
 
297
602
  await waitFor(() => {
298
603
  expect(result.current.isLoading).toBe(false);
299
- });
604
+ }, { timeout: 2000 });
300
605
 
301
606
  expect(result.current.event).toBe(null);
302
607
  expect(result.current.error).toEqual(new Error('Event not found'));
@@ -312,10 +617,69 @@ describe('usePublicEvent - Simple Tests', () => {
312
617
 
313
618
  await waitFor(() => {
314
619
  expect(result.current.isLoading).toBe(false);
315
- });
620
+ }, { timeout: 2000 });
316
621
 
317
622
  expect(result.current.event).toBe(null);
318
623
  expect(result.current.error).toEqual(new Error('Event not found'));
319
624
  });
625
+
626
+ it('handles missing Supabase environment variables', () => {
627
+ // Mock usePublicPageContext to return null environment
628
+ vi.mocked(usePublicPageContext).mockReturnValue({
629
+ environment: {
630
+ supabaseUrl: null,
631
+ supabaseKey: null
632
+ }
633
+ } as any);
634
+
635
+ // Mock environment variables to be undefined
636
+ Object.defineProperty(import.meta, 'env', {
637
+ value: {},
638
+ writable: true
639
+ });
640
+
641
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
642
+
643
+ const { result } = renderHook(() => usePublicEvent('test-event'));
644
+
645
+ expect(consoleSpy).toHaveBeenCalledWith(
646
+ '[usePublicEvent] Missing Supabase environment variables. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment.'
647
+ );
648
+
649
+ // Should still initialize but with error
650
+ expect(result.current.isLoading).toBe(false);
651
+ expect(result.current.error).toBeInstanceOf(Error);
652
+ expect(result.current.error?.message).toContain('Invalid event code or Supabase client not available');
653
+
654
+ consoleSpy.mockRestore();
655
+ });
656
+
657
+ it('handles server-side rendering (window undefined)', () => {
658
+ // Test that the hook handles missing window by checking the implementation
659
+ // The hook checks `typeof window === 'undefined'` and returns null supabase client
660
+ // We can't actually delete window in the test environment as React needs it
661
+ // Instead, we test the behavior when supabase client is null (which happens when window is undefined)
662
+
663
+ // Mock usePublicPageContext to return null environment
664
+ vi.mocked(usePublicPageContext).mockReturnValue({
665
+ environment: {
666
+ supabaseUrl: null,
667
+ supabaseKey: null
668
+ }
669
+ } as any);
670
+
671
+ // Mock environment variables to be undefined
672
+ Object.defineProperty(import.meta, 'env', {
673
+ value: {},
674
+ writable: true
675
+ });
676
+
677
+ const { result } = renderHook(() => usePublicEvent('test-event'));
678
+
679
+ // When supabase client is null (as it would be in SSR), the hook should handle it gracefully
680
+ expect(result.current.isLoading).toBe(false);
681
+ expect(result.current.error).toBeInstanceOf(Error);
682
+ expect(result.current.error?.message).toContain('Invalid event code or Supabase client not available');
683
+ });
320
684
  });
321
685
  });