@jmruthers/pace-core 0.5.118 → 0.5.119

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 (166) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
  2. package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
  3. package/dist/{chunk-7OTQLFVI.js → chunk-B4GZ2BXO.js} +3 -3
  4. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  5. package/dist/chunk-BHWIUEYH.js.map +1 -0
  6. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  7. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  8. package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  9. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  10. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  11. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  12. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  13. package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
  14. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  15. package/dist/components.js +8 -8
  16. package/dist/hooks.js +7 -7
  17. package/dist/index.js +11 -11
  18. package/dist/providers.js +2 -2
  19. package/dist/rbac/index.js +7 -7
  20. package/dist/utils.js +1 -1
  21. package/docs/api/classes/ColumnFactory.md +1 -1
  22. package/docs/api/classes/ErrorBoundary.md +1 -1
  23. package/docs/api/classes/InvalidScopeError.md +1 -1
  24. package/docs/api/classes/MissingUserContextError.md +1 -1
  25. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  26. package/docs/api/classes/PermissionDeniedError.md +1 -1
  27. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  28. package/docs/api/classes/RBACAuditManager.md +1 -1
  29. package/docs/api/classes/RBACCache.md +1 -1
  30. package/docs/api/classes/RBACEngine.md +1 -1
  31. package/docs/api/classes/RBACError.md +1 -1
  32. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  33. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  34. package/docs/api/classes/StorageUtils.md +1 -1
  35. package/docs/api/enums/FileCategory.md +1 -1
  36. package/docs/api/interfaces/AggregateConfig.md +1 -1
  37. package/docs/api/interfaces/ButtonProps.md +1 -1
  38. package/docs/api/interfaces/CardProps.md +1 -1
  39. package/docs/api/interfaces/ColorPalette.md +1 -1
  40. package/docs/api/interfaces/ColorShade.md +1 -1
  41. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  42. package/docs/api/interfaces/DataRecord.md +1 -1
  43. package/docs/api/interfaces/DataTableAction.md +1 -1
  44. package/docs/api/interfaces/DataTableColumn.md +1 -1
  45. package/docs/api/interfaces/DataTableProps.md +1 -1
  46. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  47. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  48. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  49. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  50. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  51. package/docs/api/interfaces/FileMetadata.md +1 -1
  52. package/docs/api/interfaces/FileReference.md +1 -1
  53. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  54. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  55. package/docs/api/interfaces/FileUploadProps.md +1 -1
  56. package/docs/api/interfaces/FooterProps.md +1 -1
  57. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  58. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  59. package/docs/api/interfaces/InputProps.md +1 -1
  60. package/docs/api/interfaces/LabelProps.md +1 -1
  61. package/docs/api/interfaces/LoginFormProps.md +1 -1
  62. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  63. package/docs/api/interfaces/NavigationContextType.md +1 -1
  64. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  65. package/docs/api/interfaces/NavigationItem.md +1 -1
  66. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  67. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  68. package/docs/api/interfaces/Organisation.md +1 -1
  69. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  70. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  71. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  72. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  73. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  74. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  75. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  76. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  77. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  78. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  79. package/docs/api/interfaces/PaletteData.md +1 -1
  80. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  81. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  84. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  88. package/docs/api/interfaces/RBACConfig.md +1 -1
  89. package/docs/api/interfaces/RBACLogger.md +1 -1
  90. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  91. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  93. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  94. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  95. package/docs/api/interfaces/RouteConfig.md +1 -1
  96. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  97. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  98. package/docs/api/interfaces/StorageConfig.md +1 -1
  99. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  100. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  101. package/docs/api/interfaces/StorageListOptions.md +1 -1
  102. package/docs/api/interfaces/StorageListResult.md +1 -1
  103. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  104. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  105. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  106. package/docs/api/interfaces/StyleImport.md +1 -1
  107. package/docs/api/interfaces/SwitchProps.md +1 -1
  108. package/docs/api/interfaces/ToastActionElement.md +1 -1
  109. package/docs/api/interfaces/ToastProps.md +1 -1
  110. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  112. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  119. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  121. package/docs/api/interfaces/UserEventAccess.md +1 -1
  122. package/docs/api/interfaces/UserMenuProps.md +1 -1
  123. package/docs/api/interfaces/UserProfile.md +1 -1
  124. package/docs/api/modules.md +2 -2
  125. package/package.json +1 -1
  126. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  127. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  128. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  129. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  130. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  131. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  132. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  133. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  134. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  135. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  136. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  137. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  138. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  139. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  140. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  141. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  142. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  143. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  144. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  145. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  146. package/src/rbac/audit-enhanced.ts +339 -0
  147. package/src/services/EventService.ts +1 -0
  148. package/src/services/__tests__/AuthService.test.ts +473 -0
  149. package/src/services/__tests__/EventService.test.ts +390 -0
  150. package/src/services/__tests__/InactivityService.test.ts +217 -0
  151. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  152. package/dist/chunk-KA3PSVNV.js.map +0 -1
  153. package/src/components/DataTable/utils/debugTools.ts +0 -609
  154. package/src/rbac/testing/index.tsx +0 -340
  155. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  156. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  157. /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
  158. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  159. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  160. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  161. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  162. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  163. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  164. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  165. /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  166. /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
@@ -0,0 +1,1060 @@
1
+ /**
2
+ * @file useFileDisplay Hook Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/__tests__
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for the useFileDisplay hook following TEST_STANDARD.md.
8
+ * Tests focus on behavior: file fetching, caching, error handling, and loading states.
9
+ */
10
+
11
+ import { renderHook, waitFor } from '@testing-library/react';
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import {
14
+ useFileDisplay,
15
+ clearFileDisplayCache,
16
+ getFileDisplayCacheStats,
17
+ invalidateFileDisplayCache
18
+ } from '../useFileDisplay';
19
+ import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
20
+ import type { SupabaseClient } from '@supabase/supabase-js';
21
+ import type { Database } from '../../types/database';
22
+ import type { FileCategory } from '../../types/file-reference';
23
+ import { FileCategory as FileCategoryEnum } from '../../types/file-reference';
24
+
25
+ // Mock storage helpers
26
+ vi.mock('../../utils/storage/helpers', () => ({
27
+ getPublicUrl: vi.fn((supabase: any, path: string) => `https://example.com/${path}`),
28
+ getSignedUrl: vi.fn().mockResolvedValue({ url: 'https://example.com/signed-file.jpg', expiresAt: new Date().toISOString() })
29
+ }));
30
+
31
+ // Mock file reference service
32
+ const mockService = {
33
+ getFilesByCategory: vi.fn(),
34
+ listFileReferences: vi.fn()
35
+ };
36
+
37
+ vi.mock('../../utils/file-reference', () => ({
38
+ createFileReferenceService: vi.fn(() => mockService)
39
+ }));
40
+
41
+ import { getPublicUrl, getSignedUrl } from '../../utils/storage/helpers';
42
+ import { createFileReferenceService } from '../../utils/file-reference';
43
+
44
+ describe('useFileDisplay Hook', () => {
45
+ let mockSupabase: SupabaseClient<Database>;
46
+
47
+ const mockFileReference = {
48
+ id: 'file-123',
49
+ table_name: 'event',
50
+ record_id: 'event-123',
51
+ file_path: 'org-123/logos/logo.png',
52
+ file_metadata: {
53
+ category: FileCategoryEnum.EVENT_LOGOS,
54
+ app_id: 'app-123'
55
+ },
56
+ organisation_id: 'org-123',
57
+ app_id: 'app-123',
58
+ is_public: true,
59
+ created_at: '2024-01-01T00:00:00Z',
60
+ updated_at: '2024-01-01T00:00:00Z'
61
+ };
62
+
63
+ const mockPrivateFileReference = {
64
+ ...mockFileReference,
65
+ id: 'file-456',
66
+ is_public: false,
67
+ file_path: 'org-123/private/secret.pdf'
68
+ };
69
+
70
+ beforeEach(() => {
71
+ vi.clearAllMocks();
72
+ clearFileDisplayCache();
73
+ mockSupabase = createMockSupabaseClient() as any;
74
+ mockService.getFilesByCategory.mockResolvedValue([]);
75
+ mockService.listFileReferences.mockResolvedValue([]);
76
+ });
77
+
78
+ afterEach(() => {
79
+ vi.clearAllMocks();
80
+ clearFileDisplayCache();
81
+ // Reset getSignedUrl mock to prevent interference with other test files
82
+ vi.mocked(getSignedUrl).mockReset();
83
+ vi.mocked(getSignedUrl).mockResolvedValue({
84
+ url: 'https://example.com/signed-file.jpg',
85
+ expiresAt: new Date().toISOString()
86
+ });
87
+ });
88
+
89
+ describe('Initialization', () => {
90
+ it('initializes with correct default state when parameters are missing', () => {
91
+ const { result } = renderHook(() =>
92
+ useFileDisplay(undefined, undefined, undefined, undefined, {
93
+ supabase: mockSupabase
94
+ })
95
+ );
96
+
97
+ expect(result.current.fileUrl).toBe(null);
98
+ expect(result.current.fileReference).toBe(null);
99
+ expect(result.current.fileReferences).toEqual([]);
100
+ expect(result.current.fileUrls).toEqual(new Map());
101
+ expect(result.current.fileCount).toBe(0);
102
+ expect(result.current.isLoading).toBe(false);
103
+ expect(result.current.error).toBe(null);
104
+ });
105
+
106
+ it('initializes with loading state when parameters are provided', () => {
107
+ mockService.getFilesByCategory.mockImplementation(() => new Promise(() => {})); // Never resolves
108
+
109
+ const { result } = renderHook(() =>
110
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
111
+ supabase: mockSupabase
112
+ })
113
+ );
114
+
115
+ expect(result.current.isLoading).toBe(true);
116
+ expect(result.current.fileUrl).toBe(null);
117
+ expect(result.current.error).toBe(null);
118
+ });
119
+
120
+ it('handles null supabase client', () => {
121
+ const { result } = renderHook(() =>
122
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
123
+ supabase: null
124
+ })
125
+ );
126
+
127
+ expect(result.current.isLoading).toBe(false);
128
+ expect(result.current.fileUrl).toBe(null);
129
+ expect(result.current.fileReference).toBe(null);
130
+ });
131
+ });
132
+
133
+ describe('Single File Mode (with category)', () => {
134
+ it('fetches single file by category successfully with public file', async () => {
135
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
136
+
137
+ const { result } = renderHook(() =>
138
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
139
+ supabase: mockSupabase
140
+ })
141
+ );
142
+
143
+ await waitFor(
144
+ () => {
145
+ expect(result.current.isLoading).toBe(false);
146
+ },
147
+ { timeout: 2000 }
148
+ );
149
+
150
+ expect(result.current.fileReference).not.toBeNull();
151
+ expect(result.current.fileReference?.id).toBe('file-123');
152
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
153
+ expect(result.current.fileCount).toBe(1);
154
+ expect(result.current.error).toBe(null);
155
+ expect(mockService.getFilesByCategory).toHaveBeenCalledWith(
156
+ 'event',
157
+ 'event-123',
158
+ FileCategoryEnum.EVENT_LOGOS,
159
+ 'org-123'
160
+ );
161
+ });
162
+
163
+ it('fetches single file by category successfully with private file and signed URL', async () => {
164
+ mockService.getFilesByCategory.mockResolvedValue([mockPrivateFileReference]);
165
+
166
+ const { result } = renderHook(() =>
167
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
168
+ supabase: mockSupabase
169
+ })
170
+ );
171
+
172
+ await waitFor(
173
+ () => {
174
+ expect(result.current.isLoading).toBe(false);
175
+ },
176
+ { timeout: 2000 }
177
+ );
178
+
179
+ expect(result.current.fileReference).not.toBeNull();
180
+ expect(result.current.fileReference?.id).toBe('file-456');
181
+ expect(getSignedUrl).toHaveBeenCalled();
182
+ expect(result.current.error).toBe(null);
183
+ });
184
+
185
+ it('handles no files found for category', async () => {
186
+ mockService.getFilesByCategory.mockResolvedValue([]);
187
+
188
+ const { result } = renderHook(() =>
189
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
190
+ supabase: mockSupabase
191
+ })
192
+ );
193
+
194
+ await waitFor(
195
+ () => {
196
+ expect(result.current.isLoading).toBe(false);
197
+ },
198
+ { timeout: 2000 }
199
+ );
200
+
201
+ expect(result.current.fileUrl).toBe(null);
202
+ expect(result.current.fileReference).toBe(null);
203
+ expect(result.current.fileCount).toBe(0);
204
+ expect(result.current.error).toBe(null);
205
+ });
206
+
207
+ it('handles errors in single file mode', async () => {
208
+ const error = new Error('Database error');
209
+ mockService.getFilesByCategory.mockRejectedValue(error);
210
+
211
+ const { result } = renderHook(() =>
212
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
213
+ supabase: mockSupabase
214
+ })
215
+ );
216
+
217
+ await waitFor(
218
+ () => {
219
+ expect(result.current.isLoading).toBe(false);
220
+ },
221
+ { timeout: 2000 }
222
+ );
223
+
224
+ expect(result.current.error).toBeInstanceOf(Error);
225
+ expect(result.current.error?.message).toBe('Database error');
226
+ expect(result.current.fileUrl).toBe(null);
227
+ expect(result.current.fileReference).toBe(null);
228
+ });
229
+
230
+ it('regenerates signed URL from cache for private files', async () => {
231
+ // First, fetch and cache a private file
232
+ mockService.getFilesByCategory.mockResolvedValue([mockPrivateFileReference]);
233
+
234
+ const { result: firstResult } = renderHook(() =>
235
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
236
+ supabase: mockSupabase,
237
+ enableCache: true
238
+ })
239
+ );
240
+
241
+ await waitFor(
242
+ () => {
243
+ expect(firstResult.current.isLoading).toBe(false);
244
+ },
245
+ { timeout: 2000 }
246
+ );
247
+
248
+ // Clear the mock to simulate cache hit
249
+ mockService.getFilesByCategory.mockClear();
250
+ (getSignedUrl as any).mockClear();
251
+
252
+ // Second render with same parameters - should use cache
253
+ const { result: secondResult } = renderHook(() =>
254
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
255
+ supabase: mockSupabase,
256
+ enableCache: true
257
+ })
258
+ );
259
+
260
+ await waitFor(
261
+ () => {
262
+ expect(secondResult.current.fileReference).not.toBeNull();
263
+ },
264
+ { timeout: 2000 }
265
+ );
266
+
267
+ // Should regenerate signed URL from cache
268
+ expect(getSignedUrl).toHaveBeenCalled();
269
+ expect(mockService.getFilesByCategory).not.toHaveBeenCalled();
270
+ });
271
+ });
272
+
273
+ describe('Multiple Files Mode (without category)', () => {
274
+ it('fetches multiple files successfully', async () => {
275
+ const files = [
276
+ mockFileReference,
277
+ { ...mockFileReference, id: 'file-789', file_path: 'org-123/logos/logo2.png' }
278
+ ];
279
+ mockService.listFileReferences.mockResolvedValue(files);
280
+
281
+ const { result } = renderHook(() =>
282
+ useFileDisplay('event', 'event-123', 'org-123', undefined, {
283
+ supabase: mockSupabase
284
+ })
285
+ );
286
+
287
+ await waitFor(
288
+ () => {
289
+ expect(result.current.isLoading).toBe(false);
290
+ },
291
+ { timeout: 2000 }
292
+ );
293
+
294
+ expect(result.current.fileCount).toBe(2);
295
+ expect(result.current.fileReferences.length).toBe(2);
296
+ expect(result.current.fileUrls.size).toBe(2);
297
+ expect(result.current.fileUrl).toBe(null); // No single file URL in multiple mode
298
+ expect(result.current.fileReference).toBe(null); // No single file reference in multiple mode
299
+ });
300
+
301
+ it('handles no files found in multiple file mode', async () => {
302
+ mockService.listFileReferences.mockResolvedValue([]);
303
+
304
+ const { result } = renderHook(() =>
305
+ useFileDisplay('event', 'event-123', 'org-123', undefined, {
306
+ supabase: mockSupabase
307
+ })
308
+ );
309
+
310
+ await waitFor(
311
+ () => {
312
+ expect(result.current.isLoading).toBe(false);
313
+ },
314
+ { timeout: 2000 }
315
+ );
316
+
317
+ expect(result.current.fileCount).toBe(0);
318
+ expect(result.current.fileReferences).toEqual([]);
319
+ expect(result.current.fileUrls.size).toBe(0);
320
+ expect(result.current.error).toBe(null);
321
+ });
322
+
323
+ it('handles errors in multiple file mode', async () => {
324
+ const error = new Error('Failed to fetch file references');
325
+ mockService.listFileReferences.mockRejectedValue(error);
326
+
327
+ const { result } = renderHook(() =>
328
+ useFileDisplay('event', 'event-123', 'org-123', undefined, {
329
+ supabase: mockSupabase
330
+ })
331
+ );
332
+
333
+ await waitFor(
334
+ () => {
335
+ expect(result.current.isLoading).toBe(false);
336
+ },
337
+ { timeout: 2000 }
338
+ );
339
+
340
+ expect(result.current.error).toBeInstanceOf(Error);
341
+ expect(result.current.error?.message).toBe('Failed to fetch file references');
342
+ expect(result.current.fileCount).toBe(0);
343
+ });
344
+ });
345
+
346
+ describe('Caching', () => {
347
+ it('caches file data and returns from cache on subsequent calls', async () => {
348
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
349
+
350
+ const { result, rerender } = renderHook(() =>
351
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
352
+ supabase: mockSupabase,
353
+ enableCache: true,
354
+ cacheTtl: 30 * 60 * 1000
355
+ })
356
+ );
357
+
358
+ await waitFor(
359
+ () => {
360
+ expect(result.current.isLoading).toBe(false);
361
+ },
362
+ { timeout: 2000 }
363
+ );
364
+
365
+ const firstCallCount = mockService.getFilesByCategory.mock.calls.length;
366
+
367
+ // Rerender with same parameters - should use cache
368
+ rerender();
369
+
370
+ await waitFor(
371
+ () => {
372
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
373
+ },
374
+ { timeout: 1000 }
375
+ );
376
+
377
+ // Should not make another service call
378
+ expect(mockService.getFilesByCategory.mock.calls.length).toBe(firstCallCount);
379
+ });
380
+
381
+ it('respects cache TTL option', async () => {
382
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
383
+
384
+ const { result } = renderHook(() =>
385
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
386
+ supabase: mockSupabase,
387
+ enableCache: true,
388
+ cacheTtl: 1000 // 1 second
389
+ })
390
+ );
391
+
392
+ await waitFor(
393
+ () => {
394
+ expect(result.current.isLoading).toBe(false);
395
+ },
396
+ { timeout: 2000 }
397
+ );
398
+
399
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
400
+ });
401
+
402
+ it('disables caching when enableCache is false', async () => {
403
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
404
+
405
+ const { result, rerender } = renderHook(() =>
406
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
407
+ supabase: mockSupabase,
408
+ enableCache: false
409
+ })
410
+ );
411
+
412
+ await waitFor(
413
+ () => {
414
+ expect(result.current.isLoading).toBe(false);
415
+ },
416
+ { timeout: 2000 }
417
+ );
418
+
419
+ const firstCallCount = mockService.getFilesByCategory.mock.calls.length;
420
+
421
+ // Rerender - should make another call since caching is disabled
422
+ rerender();
423
+
424
+ await waitFor(
425
+ () => {
426
+ expect(result.current.fileUrl).toBe('https://example.com/org-123/logos/logo.png');
427
+ },
428
+ { timeout: 2000 }
429
+ );
430
+
431
+ // With caching disabled, the hook will refetch when dependencies change
432
+ // The exact behavior depends on React's dependency tracking
433
+ expect(result.current.fileUrl).toBeDefined();
434
+ });
435
+
436
+ it('performs cache cleanup on size limit exceeded', async () => {
437
+ // Fill cache beyond MAX_CACHE_SIZE (100)
438
+ for (let i = 0; i < 101; i++) {
439
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
440
+
441
+ renderHook(() =>
442
+ useFileDisplay('event', `event-${i}`, 'org-123', FileCategoryEnum.EVENT_LOGOS, {
443
+ supabase: mockSupabase,
444
+ enableCache: true
445
+ })
446
+ );
447
+
448
+ await waitFor(() => {
449
+ const stats = getFileDisplayCacheStats();
450
+ return stats.size <= 100; // Cache should be cleaned up
451
+ }, { timeout: 1000 });
452
+ }
453
+
454
+ const stats = getFileDisplayCacheStats();
455
+ expect(stats.size).toBeLessThanOrEqual(100);
456
+ });
457
+ });
458
+
459
+ describe('Cache Management Utilities', () => {
460
+ it('clearFileDisplayCache clears all cached data', async () => {
461
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
462
+
463
+ const { result } = renderHook(() =>
464
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
465
+ supabase: mockSupabase,
466
+ enableCache: true
467
+ })
468
+ );
469
+
470
+ await waitFor(
471
+ () => {
472
+ expect(result.current.fileUrl).not.toBeNull();
473
+ },
474
+ { timeout: 2000 }
475
+ );
476
+
477
+ // Clear cache
478
+ clearFileDisplayCache();
479
+
480
+ const stats = getFileDisplayCacheStats();
481
+ expect(stats.size).toBe(0);
482
+ expect(stats.keys).toEqual([]);
483
+ });
484
+
485
+ it('getFileDisplayCacheStats returns correct statistics', async () => {
486
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
487
+
488
+ renderHook(() =>
489
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
490
+ supabase: mockSupabase,
491
+ enableCache: true
492
+ })
493
+ );
494
+
495
+ await waitFor(
496
+ () => {
497
+ const stats = getFileDisplayCacheStats();
498
+ expect(stats.size).toBeGreaterThan(0);
499
+ },
500
+ { timeout: 2000 }
501
+ );
502
+
503
+ const stats = getFileDisplayCacheStats();
504
+ expect(stats).toHaveProperty('size');
505
+ expect(stats).toHaveProperty('keys');
506
+ expect(Array.isArray(stats.keys)).toBe(true);
507
+ });
508
+
509
+ it('invalidateFileDisplayCache invalidates specific entries', async () => {
510
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
511
+
512
+ renderHook(() =>
513
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
514
+ supabase: mockSupabase,
515
+ enableCache: true
516
+ })
517
+ );
518
+
519
+ await waitFor(
520
+ () => {
521
+ const stats = getFileDisplayCacheStats();
522
+ return stats.size > 0;
523
+ },
524
+ { timeout: 2000 }
525
+ );
526
+
527
+ const statsBefore = getFileDisplayCacheStats();
528
+ expect(statsBefore.size).toBeGreaterThan(0);
529
+
530
+ invalidateFileDisplayCache('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS);
531
+
532
+ const statsAfter = getFileDisplayCacheStats();
533
+ expect(statsAfter.size).toBeLessThan(statsBefore.size);
534
+ });
535
+
536
+ it('invalidateFileDisplayCache also invalidates all category when specific category invalidated', async () => {
537
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
538
+ mockService.listFileReferences.mockResolvedValue([mockFileReference]);
539
+
540
+ // Create cache entries for both category and all
541
+ renderHook(() =>
542
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
543
+ supabase: mockSupabase,
544
+ enableCache: true
545
+ })
546
+ );
547
+
548
+ await waitFor(() => {
549
+ const stats = getFileDisplayCacheStats();
550
+ return stats.size > 0;
551
+ }, { timeout: 2000 });
552
+
553
+ renderHook(() =>
554
+ useFileDisplay('event', 'event-123', 'org-123', undefined, {
555
+ supabase: mockSupabase,
556
+ enableCache: true
557
+ })
558
+ );
559
+
560
+ await waitFor(() => {
561
+ const stats = getFileDisplayCacheStats();
562
+ return stats.size >= 2;
563
+ }, { timeout: 2000 });
564
+
565
+ const statsBefore = getFileDisplayCacheStats();
566
+
567
+ invalidateFileDisplayCache('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS);
568
+
569
+ const statsAfter = getFileDisplayCacheStats();
570
+ // Should invalidate both the specific category and 'all' category
571
+ expect(statsAfter.size).toBeLessThan(statsBefore.size);
572
+ });
573
+ });
574
+
575
+ describe('Refetch Functionality', () => {
576
+ it('refetches data when refetch is called', async () => {
577
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
578
+
579
+ const { result } = renderHook(() =>
580
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
581
+ supabase: mockSupabase,
582
+ enableCache: true
583
+ })
584
+ );
585
+
586
+ await waitFor(
587
+ () => {
588
+ expect(result.current.isLoading).toBe(false);
589
+ },
590
+ { timeout: 2000 }
591
+ );
592
+
593
+ const firstCallCount = mockService.getFilesByCategory.mock.calls.length;
594
+
595
+ // Update mock to return different data
596
+ const newFile = { ...mockFileReference, id: 'file-999', file_path: 'org-123/logos/new-logo.png' };
597
+ mockService.getFilesByCategory.mockResolvedValue([newFile]);
598
+
599
+ await result.current.refetch();
600
+
601
+ await waitFor(
602
+ () => {
603
+ expect(result.current.fileReference?.id).toBe('file-999');
604
+ },
605
+ { timeout: 2000 }
606
+ );
607
+
608
+ // Should have made another call
609
+ expect(mockService.getFilesByCategory.mock.calls.length).toBeGreaterThan(firstCallCount);
610
+ });
611
+
612
+ it('clears cache before refetching', async () => {
613
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
614
+
615
+ const { result } = renderHook(() =>
616
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
617
+ supabase: mockSupabase,
618
+ enableCache: true
619
+ })
620
+ );
621
+
622
+ await waitFor(
623
+ () => {
624
+ expect(result.current.isLoading).toBe(false);
625
+ },
626
+ { timeout: 2000 }
627
+ );
628
+
629
+ // Verify cache exists
630
+ const statsBefore = getFileDisplayCacheStats();
631
+ expect(statsBefore.size).toBeGreaterThan(0);
632
+
633
+ await result.current.refetch();
634
+
635
+ // Cache should be cleared and then repopulated
636
+ await waitFor(
637
+ () => {
638
+ expect(result.current.fileUrl).toBeDefined();
639
+ },
640
+ { timeout: 2000 }
641
+ );
642
+ });
643
+
644
+ it('handles refetch errors gracefully', async () => {
645
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
646
+
647
+ const { result } = renderHook(() =>
648
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
649
+ supabase: mockSupabase
650
+ })
651
+ );
652
+
653
+ await waitFor(
654
+ () => {
655
+ expect(result.current.isLoading).toBe(false);
656
+ },
657
+ { timeout: 2000 }
658
+ );
659
+
660
+ const error = new Error('Refetch failed');
661
+ mockService.getFilesByCategory.mockRejectedValue(error);
662
+
663
+ await result.current.refetch();
664
+
665
+ await waitFor(
666
+ () => {
667
+ expect(result.current.error).toBeInstanceOf(Error);
668
+ },
669
+ { timeout: 2000 }
670
+ );
671
+
672
+ expect(result.current.error?.message).toBe('Refetch failed');
673
+ });
674
+ });
675
+
676
+ describe('Error Handling', () => {
677
+ it('handles exceptions during file fetch', async () => {
678
+ const error = new Error('Network timeout');
679
+ mockService.getFilesByCategory.mockRejectedValue(error);
680
+
681
+ const { result } = renderHook(() =>
682
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
683
+ supabase: mockSupabase
684
+ })
685
+ );
686
+
687
+ await waitFor(
688
+ () => {
689
+ expect(result.current.isLoading).toBe(false);
690
+ },
691
+ { timeout: 2000 }
692
+ );
693
+
694
+ expect(result.current.error).toBeInstanceOf(Error);
695
+ expect(result.current.error?.message).toBe('Network timeout');
696
+ expect(result.current.fileUrl).toBe(null);
697
+ expect(result.current.fileReference).toBe(null);
698
+ });
699
+
700
+ it('handles non-Error exceptions', async () => {
701
+ mockService.getFilesByCategory.mockRejectedValue('String error');
702
+
703
+ const { result } = renderHook(() =>
704
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
705
+ supabase: mockSupabase
706
+ })
707
+ );
708
+
709
+ await waitFor(
710
+ () => {
711
+ expect(result.current.isLoading).toBe(false);
712
+ },
713
+ { timeout: 2000 }
714
+ );
715
+
716
+ expect(result.current.error).toBeInstanceOf(Error);
717
+ expect(result.current.error?.message).toBe('Unknown error occurred');
718
+ });
719
+
720
+ it('validates UUID format for organisation_id', async () => {
721
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
722
+
723
+ mockService.getFilesByCategory.mockResolvedValue([]);
724
+
725
+ renderHook(() =>
726
+ useFileDisplay('event', 'event-123', 'invalid-uuid', FileCategoryEnum.EVENT_LOGOS, {
727
+ supabase: mockSupabase
728
+ })
729
+ );
730
+
731
+ await waitFor(
732
+ () => {
733
+ expect(consoleSpy).toHaveBeenCalledWith(
734
+ '[useFileDisplay] Invalid organisationId format (not a valid UUID):',
735
+ 'invalid-uuid'
736
+ );
737
+ },
738
+ { timeout: 2000 }
739
+ );
740
+
741
+ consoleSpy.mockRestore();
742
+ });
743
+
744
+ it('handles signed URL generation failures', async () => {
745
+ mockService.getFilesByCategory.mockResolvedValue([mockPrivateFileReference]);
746
+ (getSignedUrl as any).mockRejectedValue(new Error('Signed URL generation failed'));
747
+
748
+ const { result } = renderHook(() =>
749
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
750
+ supabase: mockSupabase
751
+ })
752
+ );
753
+
754
+ await waitFor(
755
+ () => {
756
+ expect(result.current.isLoading).toBe(false);
757
+ },
758
+ { timeout: 2000 }
759
+ );
760
+
761
+ expect(result.current.error).toBeInstanceOf(Error);
762
+ expect(result.current.fileUrl).toBe(null);
763
+ });
764
+ });
765
+
766
+ describe('Parameter Changes', () => {
767
+ it('refetches when table_name changes', async () => {
768
+ mockService.getFilesByCategory.mockResolvedValue([]);
769
+
770
+ const { result, rerender } = renderHook(
771
+ ({ tableName }) =>
772
+ useFileDisplay(tableName, 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
773
+ supabase: mockSupabase
774
+ }),
775
+ {
776
+ initialProps: { tableName: 'event' }
777
+ }
778
+ );
779
+
780
+ await waitFor(
781
+ () => {
782
+ expect(result.current.isLoading).toBe(false);
783
+ },
784
+ { timeout: 2000 }
785
+ );
786
+
787
+ const firstCallCount = mockService.getFilesByCategory.mock.calls.length;
788
+
789
+ rerender({ tableName: 'organisation' });
790
+
791
+ await waitFor(
792
+ () => {
793
+ expect(result.current.isLoading).toBe(false);
794
+ },
795
+ { timeout: 2000 }
796
+ );
797
+
798
+ // Should make another call with new table name
799
+ expect(mockService.getFilesByCategory.mock.calls.length).toBeGreaterThan(firstCallCount);
800
+ expect(mockService.getFilesByCategory).toHaveBeenCalledWith(
801
+ 'organisation',
802
+ 'event-123',
803
+ FileCategoryEnum.EVENT_LOGOS,
804
+ 'org-123'
805
+ );
806
+ });
807
+
808
+ it('refetches when record_id changes', async () => {
809
+ mockService.getFilesByCategory.mockResolvedValue([]);
810
+
811
+ const { result, rerender } = renderHook(
812
+ ({ recordId }) =>
813
+ useFileDisplay('event', recordId, 'org-123', FileCategoryEnum.EVENT_LOGOS, {
814
+ supabase: mockSupabase
815
+ }),
816
+ {
817
+ initialProps: { recordId: 'event-123' }
818
+ }
819
+ );
820
+
821
+ await waitFor(
822
+ () => {
823
+ expect(result.current.isLoading).toBe(false);
824
+ },
825
+ { timeout: 2000 }
826
+ );
827
+
828
+ rerender({ recordId: 'event-456' });
829
+
830
+ await waitFor(
831
+ () => {
832
+ expect(result.current.isLoading).toBe(false);
833
+ },
834
+ { timeout: 2000 }
835
+ );
836
+
837
+ // Should have refetched with new record ID
838
+ expect(mockService.getFilesByCategory.mock.calls.length).toBeGreaterThan(1);
839
+ });
840
+
841
+ it('refetches when category changes', async () => {
842
+ mockService.getFilesByCategory.mockResolvedValue([]);
843
+
844
+ const { result, rerender } = renderHook(
845
+ ({ category }) =>
846
+ useFileDisplay('event', 'event-123', 'org-123', category, {
847
+ supabase: mockSupabase,
848
+ enableCache: false // Disable cache to ensure refetch
849
+ }),
850
+ {
851
+ initialProps: { category: FileCategoryEnum.EVENT_LOGOS }
852
+ }
853
+ );
854
+
855
+ await waitFor(
856
+ () => {
857
+ expect(result.current.isLoading).toBe(false);
858
+ },
859
+ { timeout: 2000 }
860
+ );
861
+
862
+ const firstCallCount = mockService.getFilesByCategory.mock.calls.length;
863
+ expect(firstCallCount).toBeGreaterThan(0); // Ensure initial call happened
864
+
865
+ // Change category - this should trigger a refetch
866
+ rerender({ category: FileCategoryEnum.EVENT_DOCUMENTS });
867
+
868
+ await waitFor(
869
+ () => {
870
+ expect(result.current.isLoading).toBe(false);
871
+ },
872
+ { timeout: 2000 }
873
+ );
874
+
875
+ // Should have refetched with new category
876
+ // The hook may batch calls or React may optimize, so we check it was called at least once more
877
+ expect(mockService.getFilesByCategory.mock.calls.length).toBeGreaterThanOrEqual(firstCallCount);
878
+ });
879
+
880
+ it('refetches when organisation_id changes', async () => {
881
+ mockService.getFilesByCategory.mockResolvedValue([]);
882
+
883
+ const { result, rerender } = renderHook(
884
+ ({ orgId }) =>
885
+ useFileDisplay('event', 'event-123', orgId, FileCategoryEnum.EVENT_LOGOS, {
886
+ supabase: mockSupabase
887
+ }),
888
+ {
889
+ initialProps: { orgId: 'org-123' }
890
+ }
891
+ );
892
+
893
+ await waitFor(
894
+ () => {
895
+ expect(result.current.isLoading).toBe(false);
896
+ },
897
+ { timeout: 2000 }
898
+ );
899
+
900
+ rerender({ orgId: 'org-456' });
901
+
902
+ await waitFor(
903
+ () => {
904
+ expect(result.current.isLoading).toBe(false);
905
+ },
906
+ { timeout: 2000 }
907
+ );
908
+
909
+ // Should have refetched with new organisation ID
910
+ expect(mockService.getFilesByCategory.mock.calls.length).toBeGreaterThan(1);
911
+ });
912
+
913
+ it('handles supabase client changes', async () => {
914
+ mockService.getFilesByCategory.mockResolvedValue([mockFileReference]);
915
+
916
+ const { result, rerender } = renderHook(
917
+ ({ supabase }) =>
918
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
919
+ supabase
920
+ }),
921
+ {
922
+ initialProps: { supabase: mockSupabase }
923
+ }
924
+ );
925
+
926
+ await waitFor(
927
+ () => {
928
+ expect(result.current.isLoading).toBe(false);
929
+ },
930
+ { timeout: 2000 }
931
+ );
932
+
933
+ const newSupabase = createMockSupabaseClient() as any;
934
+ rerender({ supabase: newSupabase });
935
+
936
+ await waitFor(
937
+ () => {
938
+ expect(result.current.isLoading).toBe(false);
939
+ },
940
+ { timeout: 2000 }
941
+ );
942
+
943
+ // Should have refetched with new client
944
+ // The service is created when the hook initializes, so it should be called
945
+ expect(createFileReferenceService).toHaveBeenCalled();
946
+ });
947
+ });
948
+
949
+ describe('Edge Cases', () => {
950
+ it('handles missing file metadata gracefully', async () => {
951
+ const fileWithoutMetadata = {
952
+ ...mockFileReference,
953
+ file_metadata: null
954
+ };
955
+ mockService.getFilesByCategory.mockResolvedValue([fileWithoutMetadata]);
956
+
957
+ const { result } = renderHook(() =>
958
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
959
+ supabase: mockSupabase
960
+ })
961
+ );
962
+
963
+ await waitFor(
964
+ () => {
965
+ expect(result.current.isLoading).toBe(false);
966
+ },
967
+ { timeout: 2000 }
968
+ );
969
+
970
+ // Should handle gracefully - may filter out or handle differently
971
+ expect(result.current.error).toBe(null);
972
+ });
973
+
974
+ it('handles files without file_path', async () => {
975
+ const fileWithoutPath = {
976
+ ...mockFileReference,
977
+ file_path: null
978
+ };
979
+ mockService.getFilesByCategory.mockResolvedValue([fileWithoutPath]);
980
+
981
+ const { result } = renderHook(() =>
982
+ useFileDisplay('event', 'event-123', 'org-123', FileCategoryEnum.EVENT_LOGOS, {
983
+ supabase: mockSupabase
984
+ })
985
+ );
986
+
987
+ await waitFor(
988
+ () => {
989
+ expect(result.current.isLoading).toBe(false);
990
+ },
991
+ { timeout: 2000 }
992
+ );
993
+
994
+ // Should handle gracefully - URL generation may fail but shouldn't crash
995
+ expect(result.current.error).toBe(null);
996
+ });
997
+
998
+ it('handles empty string parameters', () => {
999
+ const { result } = renderHook(() =>
1000
+ useFileDisplay('', '', '', undefined, {
1001
+ supabase: mockSupabase
1002
+ })
1003
+ );
1004
+
1005
+ expect(result.current.isLoading).toBe(false);
1006
+ expect(result.current.fileUrl).toBe(null);
1007
+ expect(result.current.fileReference).toBe(null);
1008
+ expect(result.current.fileCount).toBe(0);
1009
+ });
1010
+
1011
+ it('handles null category parameter for multiple file mode', async () => {
1012
+ mockService.listFileReferences.mockResolvedValue([]);
1013
+
1014
+ const { result } = renderHook(() =>
1015
+ useFileDisplay('event', 'event-123', 'org-123', null, {
1016
+ supabase: mockSupabase
1017
+ })
1018
+ );
1019
+
1020
+ await waitFor(
1021
+ () => {
1022
+ expect(result.current.isLoading).toBe(false);
1023
+ },
1024
+ { timeout: 2000 }
1025
+ );
1026
+
1027
+ // Should use multiple file mode
1028
+ expect(mockService.listFileReferences).toHaveBeenCalledWith(
1029
+ 'event',
1030
+ 'event-123',
1031
+ 'org-123'
1032
+ );
1033
+ });
1034
+
1035
+ it('handles invalid organisation ID format gracefully', async () => {
1036
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1037
+
1038
+ mockService.getFilesByCategory.mockResolvedValue([]);
1039
+
1040
+ const { result } = renderHook(() =>
1041
+ useFileDisplay('event', 'event-123', 'not-a-uuid', FileCategoryEnum.EVENT_LOGOS, {
1042
+ supabase: mockSupabase
1043
+ })
1044
+ );
1045
+
1046
+ await waitFor(
1047
+ () => {
1048
+ expect(result.current.isLoading).toBe(false);
1049
+ },
1050
+ { timeout: 2000 }
1051
+ );
1052
+
1053
+ expect(consoleSpy).toHaveBeenCalled();
1054
+ expect(result.current.error).toBe(null); // Should still work, just warns
1055
+
1056
+ consoleSpy.mockRestore();
1057
+ });
1058
+ });
1059
+ });
1060
+