@jmruthers/pace-core 0.5.117 → 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.
- package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
- package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
- package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
- package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
- package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
- package/dist/chunk-BHWIUEYH.js.map +1 -0
- package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
- package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
- package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
- package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
- package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
- package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
- package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
- package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
- package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
- package/dist/components.js +8 -8
- package/dist/hooks.js +7 -7
- package/dist/index.js +11 -11
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2 -2
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
- package/src/hooks/__tests__/index.unit.test.ts +223 -0
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
- package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
- package/src/hooks/useSecureDataAccess.test.ts +1 -0
- package/src/hooks/useSecureDataAccess.ts +43 -5
- package/src/rbac/audit-enhanced.ts +339 -0
- package/src/services/EventService.ts +1 -0
- package/src/services/__tests__/AuthService.test.ts +473 -0
- package/src/services/__tests__/EventService.test.ts +390 -0
- package/src/services/__tests__/InactivityService.test.ts +217 -0
- package/src/services/__tests__/OrganisationService.test.ts +371 -0
- package/dist/chunk-KA3PSVNV.js.map +0 -1
- package/src/components/DataTable/utils/debugTools.ts +0 -609
- package/src/rbac/testing/index.tsx +0 -340
- /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
- /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
- /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
- /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
- /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
- /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
- /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
- /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
- /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
- /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
|
+
|