@jmruthers/pace-core 0.5.186 → 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 (284) 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-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
  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-OALXJH4Y.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-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
  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-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
  23. package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
  26. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  27. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  28. package/dist/chunk-YHCN776L.js +447 -0
  29. package/dist/chunk-YHCN776L.js.map +1 -0
  30. package/dist/components.d.ts +4 -4
  31. package/dist/components.js +12 -10
  32. package/dist/components.js.map +1 -1
  33. package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
  34. package/dist/hooks.d.ts +221 -6
  35. package/dist/hooks.js +146 -49
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +24 -9
  38. package/dist/index.js +62 -28
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.js +1 -1
  41. package/dist/rbac/index.d.ts +124 -7
  42. package/dist/rbac/index.js +27 -7
  43. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  44. package/dist/types.d.ts +1 -1
  45. package/dist/types.js +1 -1
  46. package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
  47. package/dist/utils.d.ts +213 -3
  48. package/dist/utils.js +22 -2
  49. package/dist/utils.js.map +1 -1
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/Logger.md +1 -1
  54. package/docs/api/classes/MissingUserContextError.md +1 -1
  55. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  56. package/docs/api/classes/PermissionDeniedError.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +21 -17
  58. package/docs/api/classes/RBACCache.md +31 -23
  59. package/docs/api/classes/RBACEngine.md +5 -5
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/enums/LogLevel.md +1 -1
  66. package/docs/api/enums/RBACErrorCode.md +1 -1
  67. package/docs/api/enums/RPCFunction.md +1 -1
  68. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  69. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  70. package/docs/api/interfaces/AggregateConfig.md +1 -1
  71. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  72. package/docs/api/interfaces/BadgeProps.md +1 -1
  73. package/docs/api/interfaces/ButtonProps.md +1 -1
  74. package/docs/api/interfaces/CalendarProps.md +1 -1
  75. package/docs/api/interfaces/CardProps.md +1 -1
  76. package/docs/api/interfaces/ColorPalette.md +1 -1
  77. package/docs/api/interfaces/ColorShade.md +1 -1
  78. package/docs/api/interfaces/ComplianceResult.md +1 -1
  79. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  80. package/docs/api/interfaces/DataRecord.md +1 -1
  81. package/docs/api/interfaces/DataTableAction.md +1 -1
  82. package/docs/api/interfaces/DataTableColumn.md +1 -1
  83. package/docs/api/interfaces/DataTableProps.md +1 -1
  84. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  85. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  86. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  87. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  88. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  90. package/docs/api/interfaces/ExportColumn.md +1 -1
  91. package/docs/api/interfaces/ExportOptions.md +1 -1
  92. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/FormFieldProps.md +1 -1
  100. package/docs/api/interfaces/FormProps.md +1 -1
  101. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  102. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  103. package/docs/api/interfaces/InputProps.md +1 -1
  104. package/docs/api/interfaces/LabelProps.md +1 -1
  105. package/docs/api/interfaces/LoggerConfig.md +1 -1
  106. package/docs/api/interfaces/LoginFormProps.md +1 -1
  107. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  108. package/docs/api/interfaces/NavigationContextType.md +1 -1
  109. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  110. package/docs/api/interfaces/NavigationItem.md +1 -1
  111. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  112. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  113. package/docs/api/interfaces/Organisation.md +1 -1
  114. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  115. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  116. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  117. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  118. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  119. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  120. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  121. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  122. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  123. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  124. package/docs/api/interfaces/PaletteData.md +1 -1
  125. package/docs/api/interfaces/ParsedAddress.md +120 -0
  126. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  127. package/docs/api/interfaces/ProgressProps.md +1 -1
  128. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  129. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  130. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  131. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  132. package/docs/api/interfaces/QuickFix.md +1 -1
  133. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  134. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  135. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  136. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  137. package/docs/api/interfaces/RBACConfig.md +26 -3
  138. package/docs/api/interfaces/RBACContext.md +1 -1
  139. package/docs/api/interfaces/RBACLogger.md +5 -5
  140. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  141. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  142. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  144. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  146. package/docs/api/interfaces/RBACResult.md +1 -1
  147. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  148. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  153. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  154. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  155. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  156. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  157. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  158. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  159. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  160. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  161. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  162. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  163. package/docs/api/interfaces/RouteConfig.md +1 -1
  164. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  165. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  166. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  167. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  168. package/docs/api/interfaces/SetupIssue.md +1 -1
  169. package/docs/api/interfaces/StorageConfig.md +1 -1
  170. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  171. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  172. package/docs/api/interfaces/StorageListOptions.md +1 -1
  173. package/docs/api/interfaces/StorageListResult.md +1 -1
  174. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  175. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  176. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  177. package/docs/api/interfaces/StyleImport.md +1 -1
  178. package/docs/api/interfaces/SwitchProps.md +1 -1
  179. package/docs/api/interfaces/TabsContentProps.md +1 -1
  180. package/docs/api/interfaces/TabsListProps.md +1 -1
  181. package/docs/api/interfaces/TabsProps.md +1 -1
  182. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  183. package/docs/api/interfaces/TextareaProps.md +1 -1
  184. package/docs/api/interfaces/ToastActionElement.md +1 -1
  185. package/docs/api/interfaces/ToastProps.md +1 -1
  186. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  187. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  188. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  189. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  190. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  191. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  192. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  193. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  199. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  200. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  201. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  202. package/docs/api/interfaces/UserEventAccess.md +1 -1
  203. package/docs/api/interfaces/UserMenuProps.md +1 -1
  204. package/docs/api/interfaces/UserProfile.md +1 -1
  205. package/docs/api/modules.md +318 -59
  206. package/docs/best-practices/performance.md +11 -0
  207. package/docs/implementation-guides/file-upload-storage.md +29 -0
  208. package/docs/rbac/README.md +2 -1
  209. package/docs/rbac/api-reference.md +11 -0
  210. package/docs/rbac/performance.md +320 -0
  211. package/docs/standards/01-architecture-standard.md +5 -0
  212. package/docs/standards/05-security-standard.md +12 -0
  213. package/package.json +1 -1
  214. package/src/components/AddressField/AddressField.test.tsx +411 -0
  215. package/src/components/AddressField/AddressField.tsx +323 -0
  216. package/src/components/AddressField/README.md +336 -0
  217. package/src/components/AddressField/index.ts +10 -0
  218. package/src/components/AddressField/types.ts +65 -0
  219. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  220. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  221. package/src/components/index.ts +2 -0
  222. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  223. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  224. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  227. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  228. package/src/hooks/useAddressAutocomplete.ts +268 -0
  229. package/src/hooks/useFileDisplay.ts +3 -15
  230. package/src/hooks/useFileReference.test.ts +20 -3
  231. package/src/hooks/useFileReference.ts +3 -24
  232. package/src/hooks/useFileUrlCache.ts +246 -0
  233. package/src/hooks/useInactivityTracker.ts +31 -20
  234. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  235. package/src/hooks/useOrganisationSecurity.ts +3 -3
  236. package/src/hooks/useQueryCache.ts +315 -0
  237. package/src/index.ts +2 -0
  238. package/src/providers/services/EventServiceProvider.tsx +4 -1
  239. package/src/rbac/api.test.ts +21 -6
  240. package/src/rbac/api.ts +32 -11
  241. package/src/rbac/audit-batched.ts +223 -0
  242. package/src/rbac/audit-enhanced.ts +2 -2
  243. package/src/rbac/audit.test.ts +6 -5
  244. package/src/rbac/audit.ts +34 -6
  245. package/src/rbac/cache-invalidation.ts +63 -12
  246. package/src/rbac/cache.test.ts +2 -2
  247. package/src/rbac/cache.ts +61 -14
  248. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  249. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  250. package/src/rbac/config.ts +9 -0
  251. package/src/rbac/engine.ts +2 -21
  252. package/src/rbac/hooks/usePermissions.ts +21 -5
  253. package/src/rbac/index.ts +19 -0
  254. package/src/rbac/performance.ts +210 -0
  255. package/src/rbac/request-deduplication.ts +87 -0
  256. package/src/rbac/utils/deep-equal.ts +93 -0
  257. package/src/types/file-reference.ts +0 -1
  258. package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
  259. package/src/utils/file-reference/index.ts +44 -15
  260. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  261. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  262. package/src/utils/google-places/index.ts +26 -0
  263. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  264. package/src/utils/google-places/types.ts +94 -0
  265. package/src/utils/index.ts +23 -0
  266. package/src/utils/request-deduplication.ts +165 -0
  267. package/src/utils/storage/helpers.ts +143 -4
  268. package/dist/chunk-445GEP27.js.map +0 -1
  269. package/dist/chunk-FMUCXFII.js +0 -76
  270. package/dist/chunk-FMUCXFII.js.map +0 -1
  271. package/dist/chunk-FSFQFJCU.js.map +0 -1
  272. package/dist/chunk-FXFJRTKI.js.map +0 -1
  273. package/dist/chunk-GRIQLQ52.js.map +0 -1
  274. package/dist/chunk-HDCUMOOI.js.map +0 -1
  275. package/dist/chunk-OALXJH4Y.js.map +0 -1
  276. package/dist/chunk-TC7D3CR3.js.map +0 -1
  277. package/dist/chunk-U6WNSFX5.js.map +0 -1
  278. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  279. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  280. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  281. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  282. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  283. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
  284. /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
@@ -0,0 +1,403 @@
1
+ /**
2
+ * @file Google Places API Utilities Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/GooglePlaces/__tests__
5
+ * @since 0.1.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
9
+ import {
10
+ fetchPlaceAutocomplete,
11
+ fetchPlaceDetails,
12
+ parseAddressComponents,
13
+ createAddressFromPlaceResult,
14
+ getAddressByPlaceId,
15
+ } from './googlePlacesUtils';
16
+ import { clearInFlightRequests } from '../request-deduplication';
17
+ import type { GoogleAddressComponent } from './types';
18
+
19
+ // Mock loadGoogleMapsScript
20
+ vi.mock('./loadGoogleMapsScript', () => {
21
+ // Setup window.google before returning the mock
22
+ const googleMapsMock = {
23
+ maps: {
24
+ places: {
25
+ AutocompleteService: vi.fn(),
26
+ PlacesService: vi.fn(),
27
+ PlacesServiceStatus: {
28
+ OK: 'OK',
29
+ ZERO_RESULTS: 'ZERO_RESULTS',
30
+ NOT_FOUND: 'NOT_FOUND',
31
+ REQUEST_DENIED: 'REQUEST_DENIED',
32
+ INVALID_REQUEST: 'INVALID_REQUEST',
33
+ OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
34
+ },
35
+ },
36
+ LatLng: vi.fn(),
37
+ },
38
+ };
39
+
40
+ if (typeof global !== 'undefined') {
41
+ (global as any).window = {
42
+ ...(global as any).window,
43
+ google: googleMapsMock,
44
+ };
45
+ }
46
+
47
+ return {
48
+ loadGoogleMapsScript: vi.fn().mockResolvedValue(undefined),
49
+ isGoogleMapsLoaded: vi.fn(() => {
50
+ const win = typeof window !== 'undefined' ? window : (global as any).window;
51
+ return !!(win?.google?.maps?.places);
52
+ }),
53
+ };
54
+ });
55
+
56
+ // Mock window.google.maps.places
57
+ const mockAutocompleteService = {
58
+ getPlacePredictions: vi.fn(),
59
+ };
60
+
61
+ const mockPlacesService = {
62
+ getDetails: vi.fn(),
63
+ };
64
+
65
+ // Setup global window.google mock before any tests
66
+ const setupGoogleMapsMock = () => {
67
+ const googleMapsMock = {
68
+ maps: {
69
+ places: {
70
+ AutocompleteService: vi.fn(() => mockAutocompleteService),
71
+ PlacesService: vi.fn(() => mockPlacesService),
72
+ PlacesServiceStatus: {
73
+ OK: 'OK',
74
+ ZERO_RESULTS: 'ZERO_RESULTS',
75
+ NOT_FOUND: 'NOT_FOUND',
76
+ REQUEST_DENIED: 'REQUEST_DENIED',
77
+ INVALID_REQUEST: 'INVALID_REQUEST',
78
+ OVER_QUERY_LIMIT: 'OVER_QUERY_LIMIT',
79
+ },
80
+ },
81
+ LatLng: vi.fn((lat: number, lng: number) => ({
82
+ lat: () => lat,
83
+ lng: () => lng,
84
+ })),
85
+ },
86
+ };
87
+
88
+ if (typeof window !== 'undefined') {
89
+ (window as any).google = googleMapsMock;
90
+ }
91
+ if (typeof global !== 'undefined') {
92
+ (global as any).window = {
93
+ ...(global as any).window,
94
+ google: googleMapsMock,
95
+ };
96
+ }
97
+ };
98
+
99
+ // Setup immediately
100
+ setupGoogleMapsMock();
101
+
102
+ describe('Google Places API Utilities', () => {
103
+ const mockApiKey = 'test-api-key';
104
+
105
+ beforeEach(() => {
106
+ vi.clearAllMocks();
107
+ clearInFlightRequests();
108
+ // Reset mocks
109
+ mockAutocompleteService.getPlacePredictions.mockClear();
110
+ mockPlacesService.getDetails.mockClear();
111
+ });
112
+
113
+ afterEach(() => {
114
+ vi.clearAllMocks();
115
+ });
116
+
117
+ describe('fetchPlaceAutocomplete', () => {
118
+ it('fetches autocomplete predictions successfully', async () => {
119
+ const mockPredictions = [
120
+ {
121
+ description: '123 Main St, Melbourne VIC, Australia',
122
+ place_id: 'ChIJ123',
123
+ structured_formatting: {
124
+ main_text: '123 Main St',
125
+ secondary_text: 'Melbourne VIC, Australia',
126
+ },
127
+ },
128
+ ];
129
+
130
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
131
+ callback(mockPredictions, 'OK');
132
+ });
133
+
134
+ const result = await fetchPlaceAutocomplete('123 Main', mockApiKey);
135
+
136
+ expect(result).toHaveLength(1);
137
+ expect(result[0].place_id).toBe('ChIJ123');
138
+ expect(result[0].description).toBe('123 Main St, Melbourne VIC, Australia');
139
+ expect(mockAutocompleteService.getPlacePredictions).toHaveBeenCalled();
140
+ }, { timeout: 5000 });
141
+
142
+ it('returns empty array for empty query', async () => {
143
+ const result = await fetchPlaceAutocomplete('', mockApiKey);
144
+ expect(result).toEqual([]);
145
+ });
146
+
147
+ it('returns empty array for whitespace-only query', async () => {
148
+ const result = await fetchPlaceAutocomplete(' ', mockApiKey);
149
+ expect(result).toEqual([]);
150
+ });
151
+
152
+ it('throws error when API key is missing', async () => {
153
+ await expect(fetchPlaceAutocomplete('123 Main', '')).rejects.toThrow('API key is required');
154
+ });
155
+
156
+ it('handles ZERO_RESULTS status', async () => {
157
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
158
+ callback(null, 'ZERO_RESULTS');
159
+ });
160
+
161
+ const result = await fetchPlaceAutocomplete('nonexistent', mockApiKey);
162
+ expect(result).toEqual([]);
163
+ }, { timeout: 5000 });
164
+
165
+ it('handles REQUEST_DENIED status', async () => {
166
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
167
+ callback(null, 'REQUEST_DENIED');
168
+ });
169
+
170
+ await expect(fetchPlaceAutocomplete('123 Main', mockApiKey)).rejects.toThrow('REQUEST_DENIED');
171
+ }, { timeout: 5000 });
172
+
173
+ it('handles errors', async () => {
174
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
175
+ callback(null, 'INVALID_REQUEST');
176
+ });
177
+
178
+ await expect(fetchPlaceAutocomplete('123 Main', mockApiKey)).rejects.toThrow();
179
+ }, { timeout: 5000 });
180
+
181
+ it('includes optional parameters in request', async () => {
182
+ mockAutocompleteService.getPlacePredictions.mockImplementation((request, callback) => {
183
+ callback([], 'OK');
184
+ });
185
+
186
+ await fetchPlaceAutocomplete('123 Main', mockApiKey, {
187
+ components: 'country:au',
188
+ location: '-37.8136,144.9631',
189
+ radius: 5000,
190
+ types: 'address',
191
+ language: 'en',
192
+ });
193
+
194
+ expect(mockAutocompleteService.getPlacePredictions).toHaveBeenCalled();
195
+ const callArgs = mockAutocompleteService.getPlacePredictions.mock.calls[0][0];
196
+ expect(callArgs.input).toBe('123 Main');
197
+ // Country codes are converted to uppercase in the implementation
198
+ expect(callArgs.componentRestrictions).toEqual({ country: 'AU' });
199
+ expect(callArgs.radius).toBe(5000);
200
+ expect(callArgs.types).toEqual(['address']);
201
+ expect(callArgs.language).toBe('en');
202
+ }, { timeout: 5000 });
203
+ });
204
+
205
+ describe('fetchPlaceDetails', () => {
206
+ it('fetches place details successfully', async () => {
207
+ const mockPlace = {
208
+ place_id: 'ChIJ123',
209
+ formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
210
+ address_components: [
211
+ { long_name: '123', short_name: '123', types: ['street_number'] },
212
+ { long_name: 'Main Street', short_name: 'Main St', types: ['route'] },
213
+ { long_name: 'Melbourne', short_name: 'Melbourne', types: ['locality'] },
214
+ { long_name: 'Victoria', short_name: 'VIC', types: ['administrative_area_level_1'] },
215
+ { long_name: '3000', short_name: '3000', types: ['postal_code'] },
216
+ { long_name: 'Australia', short_name: 'AU', types: ['country'] },
217
+ ],
218
+ geometry: {
219
+ location: {
220
+ lat: () => -37.8136,
221
+ lng: () => 144.9631,
222
+ },
223
+ },
224
+ };
225
+
226
+ mockPlacesService.getDetails.mockImplementation((request, callback) => {
227
+ callback(mockPlace, 'OK');
228
+ });
229
+
230
+ const result = await fetchPlaceDetails('ChIJ123', mockApiKey);
231
+
232
+ expect(result.place_id).toBe('ChIJ123');
233
+ expect(result.formatted_address).toBe('123 Main St, Melbourne VIC 3000, Australia');
234
+ expect(result.geometry?.location?.lat()).toBe(-37.8136);
235
+ expect(mockPlacesService.getDetails).toHaveBeenCalled();
236
+ }, { timeout: 5000 });
237
+
238
+ it('throws error when place_id is missing', async () => {
239
+ await expect(fetchPlaceDetails('', mockApiKey)).rejects.toThrow('Place ID is required');
240
+ });
241
+
242
+ it('throws error when API key is missing', async () => {
243
+ await expect(fetchPlaceDetails('ChIJ123', '')).rejects.toThrow('API key is required');
244
+ });
245
+
246
+ it('handles NOT_FOUND status', async () => {
247
+ mockPlacesService.getDetails.mockImplementation((request, callback) => {
248
+ callback(null, 'NOT_FOUND');
249
+ });
250
+
251
+ await expect(fetchPlaceDetails('invalid', mockApiKey)).rejects.toThrow('Place not found');
252
+ }, { timeout: 5000 });
253
+ });
254
+
255
+ describe('parseAddressComponents', () => {
256
+ it('parses address components correctly', () => {
257
+ const components: GoogleAddressComponent[] = [
258
+ { long_name: '123', short_name: '123', types: ['street_number'] },
259
+ { long_name: 'Main Street', short_name: 'Main St', types: ['route'] },
260
+ { long_name: 'Melbourne', short_name: 'Melbourne', types: ['locality'] },
261
+ { long_name: 'Victoria', short_name: 'VIC', types: ['administrative_area_level_1'] },
262
+ { long_name: '3000', short_name: '3000', types: ['postal_code'] },
263
+ { long_name: 'Australia', short_name: 'AU', types: ['country'] },
264
+ ];
265
+
266
+ const result = parseAddressComponents(components);
267
+
268
+ expect(result.street_number).toBe('123');
269
+ expect(result.route).toBe('Main Street');
270
+ expect(result.suburb).toBe('Melbourne');
271
+ expect(result.state).toBe('VIC');
272
+ expect(result.postcode).toBe('3000');
273
+ expect(result.country).toBe('AU');
274
+ });
275
+
276
+ it('handles missing components', () => {
277
+ const components: GoogleAddressComponent[] = [
278
+ { long_name: 'Melbourne', short_name: 'Melbourne', types: ['locality'] },
279
+ ];
280
+
281
+ const result = parseAddressComponents(components);
282
+
283
+ expect(result.street_number).toBeNull();
284
+ expect(result.route).toBeNull();
285
+ expect(result.suburb).toBe('Melbourne');
286
+ expect(result.state).toBeNull();
287
+ expect(result.postcode).toBeNull();
288
+ expect(result.country).toBeNull();
289
+ });
290
+
291
+ it('handles empty components array', () => {
292
+ const result = parseAddressComponents([]);
293
+
294
+ expect(result.street_number).toBeNull();
295
+ expect(result.route).toBeNull();
296
+ expect(result.suburb).toBeNull();
297
+ expect(result.state).toBeNull();
298
+ expect(result.postcode).toBeNull();
299
+ expect(result.country).toBeNull();
300
+ });
301
+
302
+ it('prefers locality over sublocality', () => {
303
+ const components: GoogleAddressComponent[] = [
304
+ { long_name: 'Sublocality', short_name: 'Sublocality', types: ['sublocality'] },
305
+ { long_name: 'Locality', short_name: 'Locality', types: ['locality'] },
306
+ ];
307
+
308
+ const result = parseAddressComponents(components);
309
+ expect(result.suburb).toBe('Locality');
310
+ });
311
+ });
312
+
313
+ describe('createAddressFromPlaceResult', () => {
314
+ it('creates parsed address from place result', () => {
315
+ const placeResult = {
316
+ place_id: 'ChIJ123',
317
+ formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
318
+ address_components: [
319
+ { long_name: '123', short_name: '123', types: ['street_number'] },
320
+ { long_name: 'Main Street', short_name: 'Main St', types: ['route'] },
321
+ { long_name: 'Melbourne', short_name: 'Melbourne', types: ['locality'] },
322
+ { long_name: 'Victoria', short_name: 'VIC', types: ['administrative_area_level_1'] },
323
+ { long_name: '3000', short_name: '3000', types: ['postal_code'] },
324
+ { long_name: 'Australia', short_name: 'AU', types: ['country'] },
325
+ ],
326
+ geometry: {
327
+ location: {
328
+ lat: () => -37.8136,
329
+ lng: () => 144.9631,
330
+ },
331
+ },
332
+ };
333
+
334
+ const result = createAddressFromPlaceResult(placeResult);
335
+
336
+ expect(result.place_id).toBe('ChIJ123');
337
+ expect(result.full_address).toBe('123 Main St, Melbourne VIC 3000, Australia');
338
+ expect(result.street_number).toBe('123');
339
+ expect(result.route).toBe('Main Street');
340
+ expect(result.suburb).toBe('Melbourne');
341
+ expect(result.state).toBe('VIC');
342
+ expect(result.postcode).toBe('3000');
343
+ expect(result.country).toBe('AU');
344
+ expect(result.lat).toBe(-37.8136);
345
+ expect(result.lng).toBe(144.9631);
346
+ });
347
+
348
+ it('handles missing geometry', () => {
349
+ const placeResult = {
350
+ place_id: 'ChIJ123',
351
+ formatted_address: '123 Main St',
352
+ address_components: [],
353
+ geometry: {},
354
+ };
355
+
356
+ const result = createAddressFromPlaceResult(placeResult);
357
+
358
+ expect(result.place_id).toBe('ChIJ123');
359
+ expect(result.lat).toBeNull();
360
+ expect(result.lng).toBeNull();
361
+ });
362
+ });
363
+
364
+ describe('getAddressByPlaceId', () => {
365
+ it('retrieves address by place_id successfully', async () => {
366
+ const mockPlace = {
367
+ place_id: 'ChIJ123',
368
+ formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
369
+ address_components: [
370
+ { long_name: '123', short_name: '123', types: ['street_number'] },
371
+ { long_name: 'Main Street', short_name: 'Main St', types: ['route'] },
372
+ { long_name: 'Melbourne', short_name: 'Melbourne', types: ['locality'] },
373
+ ],
374
+ geometry: {
375
+ location: {
376
+ lat: () => -37.8136,
377
+ lng: () => 144.9631,
378
+ },
379
+ },
380
+ };
381
+
382
+ mockPlacesService.getDetails.mockImplementation((request, callback) => {
383
+ callback(mockPlace, 'OK');
384
+ });
385
+
386
+ const result = await getAddressByPlaceId('ChIJ123', mockApiKey);
387
+
388
+ expect(result).not.toBeNull();
389
+ expect(result?.place_id).toBe('ChIJ123');
390
+ expect(result?.full_address).toBe('123 Main St, Melbourne VIC 3000, Australia');
391
+ }, { timeout: 5000 });
392
+
393
+ it('returns null on error', async () => {
394
+ mockPlacesService.getDetails.mockImplementation((request, callback) => {
395
+ callback(null, 'NOT_FOUND');
396
+ });
397
+
398
+ const result = await getAddressByPlaceId('ChIJ123', mockApiKey);
399
+ expect(result).toBeNull();
400
+ }, { timeout: 5000 });
401
+ });
402
+ });
403
+