@jmruthers/pace-core 0.5.185 → 0.5.187

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 (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -0,0 +1,318 @@
1
+ /**
2
+ * @file Address Autocomplete Hook Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/__tests__
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+ import { renderHook, waitFor } from '@testing-library/react';
10
+ import { useAddressAutocomplete } from './useAddressAutocomplete';
11
+ import * as googlePlacesUtils from '../utils/google-places';
12
+
13
+ // Mock the Google Places utilities
14
+ vi.mock('../utils/google-places', () => ({
15
+ fetchPlaceAutocomplete: vi.fn(),
16
+ fetchPlaceDetails: vi.fn(),
17
+ createAddressFromPlaceResult: vi.fn(),
18
+ getAddressByPlaceId: vi.fn(),
19
+ }));
20
+
21
+ // Mock useQueryCache
22
+ vi.mock('./useQueryCache', () => ({
23
+ useQueryCache: () => ({
24
+ getCachedQuery: async <T,>(_table: string, _filterKey: string, _filterValue: string, fetchFn: () => Promise<T>) => {
25
+ return fetchFn();
26
+ },
27
+ }),
28
+ }));
29
+
30
+ // Mock useDebounce to return value immediately for tests
31
+ vi.mock('./useDebounce', () => ({
32
+ useDebounce: (value: any, _delay: number) => value,
33
+ }));
34
+
35
+ describe('useAddressAutocomplete', () => {
36
+ const mockApiKey = 'test-api-key';
37
+
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ afterEach(() => {
43
+ vi.clearAllMocks();
44
+ vi.useRealTimers();
45
+ });
46
+
47
+ describe('Initial state', () => {
48
+ it('returns initial state with empty suggestions', () => {
49
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
50
+
51
+ expect(result.current.suggestions).toEqual([]);
52
+ expect(result.current.isLoading).toBe(false);
53
+ expect(result.current.error).toBeNull();
54
+ }, { timeout: 5000 });
55
+ });
56
+
57
+ describe('Autocomplete suggestions', () => {
58
+ it('fetches suggestions when input value changes', async () => {
59
+ const mockPredictions = [
60
+ {
61
+ description: '123 Main St, Melbourne VIC, Australia',
62
+ place_id: 'ChIJ123',
63
+ structured_formatting: {
64
+ main_text: '123 Main St',
65
+ secondary_text: 'Melbourne VIC, Australia',
66
+ },
67
+ },
68
+ ];
69
+
70
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
71
+
72
+ const { result } = renderHook(() =>
73
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
74
+ );
75
+
76
+ await waitFor(
77
+ () => {
78
+ expect(result.current.suggestions).toHaveLength(1);
79
+ },
80
+ { timeout: 1000 }
81
+ );
82
+
83
+ expect(result.current.suggestions[0].place_id).toBe('ChIJ123');
84
+ }, { timeout: 5000 });
85
+
86
+ it('debounces input value', async () => {
87
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValue([]);
88
+
89
+ // Since we're mocking useDebounce to return immediately, this test verifies
90
+ // that the hook still works correctly with rapid input changes
91
+ const { result, rerender } = renderHook(
92
+ ({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
93
+ { initialProps: { inputValue: '123' } }
94
+ );
95
+
96
+ // Change input quickly - with mocked debounce, each change triggers immediately
97
+ rerender({ inputValue: '123 M' });
98
+ rerender({ inputValue: '123 Ma' });
99
+ rerender({ inputValue: '123 Main' });
100
+
101
+ // Wait for the last call
102
+ await waitFor(
103
+ () => {
104
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
105
+ },
106
+ { timeout: 1000 }
107
+ );
108
+
109
+ // With mocked debounce, it will be called for each change
110
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
111
+ }, { timeout: 5000 });
112
+
113
+ it('clears suggestions when input is empty', async () => {
114
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
115
+ { description: '123 Main St', place_id: 'ChIJ123' },
116
+ ]);
117
+
118
+ const { result, rerender } = renderHook(
119
+ ({ inputValue }) => useAddressAutocomplete(mockApiKey, inputValue, { debounceDelay: 0 }),
120
+ { initialProps: { inputValue: '123 Main' } }
121
+ );
122
+
123
+ await waitFor(
124
+ () => {
125
+ expect(result.current.suggestions).toBeDefined();
126
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
127
+ },
128
+ { timeout: 1000 }
129
+ );
130
+
131
+ rerender({ inputValue: '' });
132
+
133
+ await waitFor(
134
+ () => {
135
+ expect(result.current.suggestions).toBeDefined();
136
+ expect(result.current.suggestions).toEqual([]);
137
+ },
138
+ { timeout: 1000 }
139
+ );
140
+ }, { timeout: 5000 });
141
+
142
+ it('handles API errors', async () => {
143
+ const error = new Error('API request denied');
144
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockRejectedValueOnce(error);
145
+
146
+ const { result } = renderHook(() =>
147
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
148
+ );
149
+
150
+ await waitFor(
151
+ () => {
152
+ expect(result.current.error).not.toBeNull();
153
+ },
154
+ { timeout: 1000 }
155
+ );
156
+
157
+ expect(result.current.error?.message).toBe('API request denied');
158
+ expect(result.current.suggestions).toEqual([]);
159
+ }, { timeout: 5000 });
160
+ });
161
+
162
+ describe('Address selection', () => {
163
+ it('selects address by place_id', async () => {
164
+ const mockPlaceDetails = {
165
+ place_id: 'ChIJ123',
166
+ formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
167
+ address_components: [],
168
+ geometry: { location: { lat: -37.8136, lng: 144.9631 } },
169
+ };
170
+
171
+ const mockParsedAddress = {
172
+ place_id: 'ChIJ123',
173
+ full_address: '123 Main St, Melbourne VIC 3000, Australia',
174
+ street_number: null,
175
+ route: null,
176
+ suburb: null,
177
+ state: null,
178
+ postcode: null,
179
+ country: null,
180
+ lat: -37.8136,
181
+ lng: 144.9631,
182
+ };
183
+
184
+ vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockResolvedValueOnce(mockPlaceDetails);
185
+ vi.mocked(googlePlacesUtils.createAddressFromPlaceResult).mockReturnValueOnce(mockParsedAddress);
186
+
187
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
188
+
189
+ const address = await result.current.selectAddress('ChIJ123');
190
+
191
+ expect(address).not.toBeNull();
192
+ expect(address?.place_id).toBe('ChIJ123');
193
+ expect(googlePlacesUtils.fetchPlaceDetails).toHaveBeenCalledWith('ChIJ123', mockApiKey);
194
+ }, { timeout: 5000 });
195
+
196
+ it('returns null when place_id is invalid', async () => {
197
+ vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockRejectedValueOnce(new Error('Place not found'));
198
+
199
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
200
+
201
+ const address = await result.current.selectAddress('invalid');
202
+
203
+ expect(address).toBeNull();
204
+ });
205
+ });
206
+
207
+ describe('getAddressByPlaceId', () => {
208
+ it('retrieves address by place_id', async () => {
209
+ const mockAddress = {
210
+ place_id: 'ChIJ123',
211
+ full_address: '123 Main St',
212
+ street_number: null,
213
+ route: null,
214
+ suburb: null,
215
+ state: null,
216
+ postcode: null,
217
+ country: null,
218
+ lat: null,
219
+ lng: null,
220
+ };
221
+
222
+ vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(mockAddress);
223
+
224
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
225
+
226
+ const address = await result.current.getAddressByPlaceId('ChIJ123');
227
+
228
+ expect(address).not.toBeNull();
229
+ expect(address?.place_id).toBe('ChIJ123');
230
+ }, { timeout: 5000 });
231
+
232
+ it('returns null on error', async () => {
233
+ vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(null);
234
+
235
+ const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
236
+
237
+ const address = await result.current.getAddressByPlaceId('invalid');
238
+
239
+ expect(address).toBeNull();
240
+ }, { timeout: 5000 });
241
+ });
242
+
243
+ describe('clearSuggestions', () => {
244
+ it('clears suggestions', async () => {
245
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
246
+ { description: '123 Main St', place_id: 'ChIJ123' },
247
+ ]);
248
+
249
+ const { result } = renderHook(() =>
250
+ useAddressAutocomplete(mockApiKey, '123 Main', { debounceDelay: 0 })
251
+ );
252
+
253
+ await waitFor(
254
+ () => {
255
+ expect(result.current.suggestions).toBeDefined();
256
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
257
+ },
258
+ { timeout: 1000 }
259
+ );
260
+
261
+ result.current.clearSuggestions();
262
+
263
+ // Wait for state update
264
+ await waitFor(
265
+ () => {
266
+ expect(result.current.suggestions).toEqual([]);
267
+ expect(result.current.error).toBeNull();
268
+ },
269
+ { timeout: 1000 }
270
+ );
271
+ }, { timeout: 5000 });
272
+ });
273
+
274
+ describe('Caching', () => {
275
+ it('uses cache when enabled', async () => {
276
+ const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
277
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
278
+
279
+ const { result } = renderHook(() =>
280
+ useAddressAutocomplete(mockApiKey, '123 Main', {
281
+ debounceDelay: 0,
282
+ cacheEnabled: true,
283
+ })
284
+ );
285
+
286
+ await waitFor(
287
+ () => {
288
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
289
+ },
290
+ { timeout: 1000 }
291
+ );
292
+
293
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
294
+ });
295
+
296
+ it('skips cache when disabled', async () => {
297
+ const mockPredictions = [{ description: '123 Main St', place_id: 'ChIJ123' }];
298
+ vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce(mockPredictions);
299
+
300
+ const { result } = renderHook(() =>
301
+ useAddressAutocomplete(mockApiKey, '123 Main', {
302
+ debounceDelay: 0,
303
+ cacheEnabled: false,
304
+ })
305
+ );
306
+
307
+ await waitFor(
308
+ () => {
309
+ expect(result.current.suggestions.length).toBeGreaterThan(0);
310
+ },
311
+ { timeout: 1000 }
312
+ );
313
+
314
+ expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
315
+ }, { timeout: 5000 });
316
+ });
317
+ });
318
+
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @file Address Autocomplete Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/AddressAutocomplete
5
+ * @since 0.1.0
6
+ *
7
+ * Hook for managing Google Places API autocomplete functionality.
8
+ * Provides debounced input, caching, and address selection.
9
+ */
10
+
11
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
12
+ import { useDebounce } from './useDebounce';
13
+ import { useQueryCache } from './useQueryCache';
14
+ import {
15
+ fetchPlaceAutocomplete,
16
+ fetchPlaceDetails,
17
+ createAddressFromPlaceResult,
18
+ getAddressByPlaceId,
19
+ } from '../utils/google-places';
20
+ import type {
21
+ GooglePlaceAutocompletePrediction,
22
+ ParsedAddress,
23
+ AutocompleteOptions,
24
+ } from '../utils/google-places';
25
+
26
+ export interface UseAddressAutocompleteOptions {
27
+ /** Google Places API options */
28
+ autocompleteOptions?: AutocompleteOptions;
29
+ /** Debounce delay in milliseconds (default: 300) */
30
+ debounceDelay?: number;
31
+ /** Enable caching (default: true) */
32
+ cacheEnabled?: boolean;
33
+ /** Cache TTL configuration */
34
+ cacheTTL?: {
35
+ /** Autocomplete cache TTL in seconds (default: 3600 = 1 hour) */
36
+ autocomplete?: number;
37
+ /** Place details cache TTL in seconds (default: 86400 = 24 hours) */
38
+ placeDetails?: number;
39
+ };
40
+ }
41
+
42
+ export interface UseAddressAutocompleteReturn {
43
+ /** Array of autocomplete suggestions */
44
+ suggestions: GooglePlaceAutocompletePrediction[];
45
+ /** Loading state */
46
+ isLoading: boolean;
47
+ /** Error state */
48
+ error: Error | null;
49
+ /** Select an address by place_id */
50
+ selectAddress: (placeId: string) => Promise<ParsedAddress | null>;
51
+ /** Get address by place_id (uses cache) */
52
+ getAddressByPlaceId: (placeId: string) => Promise<ParsedAddress | null>;
53
+ /** Clear suggestions */
54
+ clearSuggestions: () => void;
55
+ }
56
+
57
+ /**
58
+ * Hook for Google Places API autocomplete with caching
59
+ *
60
+ * @param apiKey - Google Places API key
61
+ * @param inputValue - Current input value
62
+ * @param options - Hook configuration options
63
+ * @returns Autocomplete state and functions
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * const { suggestions, isLoading, selectAddress } = useAddressAutocomplete(
68
+ * apiKey,
69
+ * inputValue,
70
+ * { debounceDelay: 300, cacheEnabled: true }
71
+ * );
72
+ * ```
73
+ */
74
+ export function useAddressAutocomplete(
75
+ apiKey: string,
76
+ inputValue: string,
77
+ options: UseAddressAutocompleteOptions = {}
78
+ ): UseAddressAutocompleteReturn {
79
+ const {
80
+ debounceDelay = 300,
81
+ cacheEnabled = true,
82
+ cacheTTL = {
83
+ autocomplete: 3600, // 1 hour
84
+ placeDetails: 86400, // 24 hours
85
+ },
86
+ autocompleteOptions,
87
+ } = options;
88
+
89
+ const [suggestions, setSuggestions] = useState<GooglePlaceAutocompletePrediction[]>([]);
90
+ const [isLoading, setIsLoading] = useState(false);
91
+ const [error, setError] = useState<Error | null>(null);
92
+
93
+ const debouncedInput = useDebounce(inputValue, debounceDelay);
94
+ const { getCachedQuery } = useQueryCache();
95
+ const abortControllerRef = useRef<AbortController | null>(null);
96
+
97
+ // Memoize autocompleteOptions to prevent unnecessary re-renders
98
+ const memoizedAutocompleteOptions = useMemo(
99
+ () => autocompleteOptions,
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ [JSON.stringify(autocompleteOptions)]
102
+ );
103
+
104
+ // Fetch autocomplete suggestions
105
+ useEffect(() => {
106
+ // Cancel previous request if still in flight
107
+ if (abortControllerRef.current) {
108
+ abortControllerRef.current.abort();
109
+ }
110
+
111
+ // Reset state if input is empty
112
+ if (!debouncedInput.trim()) {
113
+ setSuggestions([]);
114
+ setIsLoading(false);
115
+ setError(null);
116
+ return;
117
+ }
118
+
119
+ // Don't fetch if no API key
120
+ if (!apiKey) {
121
+ setError(new Error('Google Places API key is required'));
122
+ return;
123
+ }
124
+
125
+ setIsLoading(true);
126
+ setError(null);
127
+
128
+ const fetchSuggestions = async () => {
129
+ try {
130
+ let predictions: GooglePlaceAutocompletePrediction[];
131
+
132
+ if (cacheEnabled) {
133
+ // Use cached query with TTL
134
+ predictions = await getCachedQuery(
135
+ 'google-places-autocomplete',
136
+ 'query',
137
+ debouncedInput,
138
+ async () => {
139
+ return fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
140
+ },
141
+ { ttl: cacheTTL.autocomplete, enabled: true }
142
+ );
143
+ } else {
144
+ // Direct fetch without cache
145
+ predictions = await fetchPlaceAutocomplete(debouncedInput, apiKey, memoizedAutocompleteOptions);
146
+ }
147
+
148
+ setSuggestions(predictions);
149
+ setIsLoading(false);
150
+ } catch (err) {
151
+ // Ignore aborted requests
152
+ if (err instanceof Error && err.name === 'AbortError') {
153
+ return;
154
+ }
155
+
156
+ const error = err instanceof Error ? err : new Error('Failed to fetch autocomplete suggestions');
157
+ setError(error);
158
+ setSuggestions([]);
159
+ setIsLoading(false);
160
+ }
161
+ };
162
+
163
+ fetchSuggestions();
164
+
165
+ // Cleanup function to cancel in-flight requests
166
+ return () => {
167
+ if (abortControllerRef.current) {
168
+ abortControllerRef.current.abort();
169
+ }
170
+ };
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [debouncedInput, apiKey, cacheEnabled, cacheTTL.autocomplete]);
173
+
174
+ // Select address by place_id
175
+ const selectAddress = useCallback(
176
+ async (placeId: string): Promise<ParsedAddress | null> => {
177
+ if (!placeId || !apiKey) {
178
+ return null;
179
+ }
180
+
181
+ setIsLoading(true);
182
+ setError(null);
183
+
184
+ try {
185
+ let placeDetails;
186
+
187
+ if (cacheEnabled) {
188
+ // Use cached query with TTL
189
+ placeDetails = await getCachedQuery(
190
+ 'google-places-details',
191
+ 'place_id',
192
+ placeId,
193
+ async () => {
194
+ return fetchPlaceDetails(placeId, apiKey);
195
+ },
196
+ { ttl: cacheTTL.placeDetails, enabled: true }
197
+ );
198
+ } else {
199
+ // Direct fetch without cache
200
+ placeDetails = await fetchPlaceDetails(placeId, apiKey);
201
+ }
202
+
203
+ const parsedAddress = createAddressFromPlaceResult(placeDetails);
204
+ setIsLoading(false);
205
+ return parsedAddress;
206
+ } catch (err) {
207
+ const error = err instanceof Error ? err : new Error('Failed to fetch place details');
208
+ setError(error);
209
+ setIsLoading(false);
210
+ return null;
211
+ }
212
+ },
213
+ // eslint-disable-next-line react-hooks/exhaustive-deps
214
+ [apiKey, cacheEnabled, cacheTTL.placeDetails]
215
+ );
216
+
217
+ // Get address by place_id (utility function)
218
+ const getAddressByPlaceIdFn = useCallback(
219
+ async (placeId: string): Promise<ParsedAddress | null> => {
220
+ if (!placeId || !apiKey) {
221
+ return null;
222
+ }
223
+
224
+ try {
225
+ if (cacheEnabled) {
226
+ // Use cached query with TTL
227
+ return await getCachedQuery(
228
+ 'google-places-details',
229
+ 'place_id',
230
+ placeId,
231
+ async () => {
232
+ const result = await getAddressByPlaceId(placeId, apiKey);
233
+ if (!result) {
234
+ throw new Error('Failed to fetch address');
235
+ }
236
+ return result;
237
+ },
238
+ { ttl: cacheTTL.placeDetails, enabled: true }
239
+ );
240
+ } else {
241
+ return await getAddressByPlaceId(placeId, apiKey);
242
+ }
243
+ } catch (err) {
244
+ const error = err instanceof Error ? err : new Error('Failed to get address by place_id');
245
+ setError(error);
246
+ return null;
247
+ }
248
+ },
249
+ // eslint-disable-next-line react-hooks/exhaustive-deps
250
+ [apiKey, cacheEnabled, cacheTTL.placeDetails]
251
+ );
252
+
253
+ // Clear suggestions
254
+ const clearSuggestions = useCallback(() => {
255
+ setSuggestions([]);
256
+ setError(null);
257
+ }, []);
258
+
259
+ return {
260
+ suggestions,
261
+ isLoading,
262
+ error,
263
+ selectAddress,
264
+ getAddressByPlaceId: getAddressByPlaceIdFn,
265
+ clearSuggestions,
266
+ };
267
+ }
268
+
@@ -37,7 +37,7 @@
37
37
  import { useState, useEffect, useCallback } from 'react';
38
38
  import type { SupabaseClient } from '@supabase/supabase-js';
39
39
  import { FileReference, FileCategory } from '../types/file-reference';
40
- import { getPublicUrl, getSignedUrl } from '../utils/storage/helpers';
40
+ import { getPublicUrl, getSignedUrl, generateFileUrlsBatch } from '../utils/storage/helpers';
41
41
  import { createFileReferenceService } from '../utils/file-reference';
42
42
  import { logger } from '../utils/core/logger';
43
43
 
@@ -279,24 +279,12 @@ export function useFileDisplay(
279
279
  logger.debug('useFileDisplay', 'Setting file URL:', url ? 'URL set' : 'URL is null');
280
280
  setFileUrl(url);
281
281
  } else {
282
- // Multiple files mode - generate URLs for all files
283
- const urlMap = new Map<string, string>();
284
- for (const fileRef of files) {
285
- let url: string | null = null;
286
- if (fileRef.is_public) {
287
- url = getPublicUrl(supabase, fileRef.file_path, true);
288
- } else {
289
- const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
282
+ // Multiple files mode - generate URLs for all files in batch
283
+ const urlMap = await generateFileUrlsBatch(supabase, files, {
290
284
  appName: 'pace-core',
291
285
  orgId: organisation_id,
292
286
  expiresIn: 3600
293
287
  });
294
- url = signedUrlResult?.url || null;
295
- }
296
- if (url) {
297
- urlMap.set(fileRef.id, url);
298
- }
299
- }
300
288
  setFileUrls(urlMap);
301
289
  setFileReference(null);
302
290
  setFileUrl(null);
@@ -18,6 +18,7 @@ let serviceMock: any;
18
18
  const mockUploadFileWithReference = vi.fn();
19
19
  const mockGetPublicUrl = vi.fn();
20
20
  const mockGetSignedUrl = vi.fn();
21
+ const mockGenerateFileUrlsBatch = vi.fn();
21
22
 
22
23
  // Setup mocks
23
24
  beforeEach(() => {
@@ -47,12 +48,25 @@ beforeEach(() => {
47
48
  url: 'https://example.com/signed-file.jpg',
48
49
  expiresAt: new Date(Date.now() + 3600 * 1000).toISOString()
49
50
  });
51
+ // Mock batch URL generation to return URLs for all files
52
+ mockGenerateFileUrlsBatch.mockImplementation(async (supabase, files) => {
53
+ const urlMap = new Map<string, string>();
54
+ for (const file of files) {
55
+ if (file.is_public) {
56
+ urlMap.set(file.id, `https://example.com/${file.file_path}`);
57
+ } else {
58
+ urlMap.set(file.id, `https://example.com/signed/${file.file_path}`);
59
+ }
60
+ }
61
+ return urlMap;
62
+ });
50
63
 
51
64
  // Apply mocks
52
65
  vi.spyOn(fileReferenceUtils, 'createFileReferenceService').mockImplementation(mockCreateFileReferenceService as any);
53
66
  vi.spyOn(fileReferenceUtils, 'uploadFileWithReference').mockImplementation(mockUploadFileWithReference as any);
54
67
  vi.spyOn(storageHelpers, 'getPublicUrl').mockImplementation(mockGetPublicUrl as any);
55
68
  vi.spyOn(storageHelpers, 'getSignedUrl').mockImplementation(mockGetSignedUrl as any);
69
+ vi.spyOn(storageHelpers, 'generateFileUrlsBatch').mockImplementation(mockGenerateFileUrlsBatch as any);
56
70
  });
57
71
 
58
72
  afterEach(() => {
@@ -67,6 +81,7 @@ const mockFileUploadOptions = {
67
81
  organisation_id: 'test-org-123',
68
82
  app_id: 'test-app-123',
69
83
  category: FileCategory.GENERAL_DOCUMENTS,
84
+ folder: 'documents',
70
85
  pageContext: 'configuration',
71
86
  is_public: false
72
87
  };
@@ -504,16 +519,19 @@ describe('[hook] useFilesByCategory', () => {
504
519
  );
505
520
 
506
521
  await waitFor(() => {
507
- expect(mockGetPublicUrl).toHaveBeenCalledWith(mockSupabase, publicFile.file_path, true);
508
- expect(mockGetSignedUrl).toHaveBeenCalledWith(
522
+ // Verify batch URL generation was called with both files
523
+ expect(mockGenerateFileUrlsBatch).toHaveBeenCalledWith(
509
524
  mockSupabase,
510
- privateFile.file_path,
525
+ [publicFile, privateFile],
511
526
  expect.objectContaining({
512
527
  appName: 'file-reference',
513
528
  orgId: 'test-org-123',
514
529
  expiresIn: 3600
515
530
  })
516
531
  );
532
+ // Verify URLs were generated
533
+ expect(result.current.fileUrls.get(publicFile.id)).toBeDefined();
534
+ expect(result.current.fileUrls.get(privateFile.id)).toBeDefined();
517
535
  });
518
536
  });
519
537
  });