@jmruthers/pace-core 0.5.184 → 0.5.186

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 (319) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +23 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-W22JP75J.js → chunk-DAGICKHT.js} +9 -7
  13. package/dist/chunk-DAGICKHT.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-CSOFYHAG.js → chunk-GRIQLQ52.js} +374 -60
  17. package/dist/chunk-GRIQLQ52.js.map +1 -0
  18. package/dist/{chunk-NQPMQGS2.js → chunk-HDCUMOOI.js} +497 -399
  19. package/dist/chunk-HDCUMOOI.js.map +1 -0
  20. package/dist/chunk-HESYZWZW.js +388 -0
  21. package/dist/chunk-HESYZWZW.js.map +1 -0
  22. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  23. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  24. package/dist/{chunk-PWAHJW4G.js → chunk-OALXJH4Y.js} +86 -33
  25. package/dist/chunk-OALXJH4Y.js.map +1 -0
  26. package/dist/{chunk-MI7HBHN3.js → chunk-TC7D3CR3.js} +89 -9
  27. package/dist/chunk-TC7D3CR3.js.map +1 -0
  28. package/dist/chunk-THRPYOFK.js +215 -0
  29. package/dist/chunk-THRPYOFK.js.map +1 -0
  30. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  31. package/dist/chunk-U6WNSFX5.js.map +1 -0
  32. package/dist/{chunk-UHNYIBXL.js → chunk-UQWSHFVX.js} +1 -1
  33. package/dist/chunk-UQWSHFVX.js.map +1 -0
  34. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  35. package/dist/components.d.ts +182 -6
  36. package/dist/components.js +157 -11
  37. package/dist/components.js.map +1 -1
  38. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  39. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  40. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-PRTSLxKx.d.ts} +10 -1
  41. package/dist/hooks.d.ts +52 -15
  42. package/dist/hooks.js +12 -22
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +12 -12
  45. package/dist/index.js +82 -18
  46. package/dist/index.js.map +1 -1
  47. package/dist/providers.d.ts +1 -1
  48. package/dist/providers.js +3 -1
  49. package/dist/rbac/index.d.ts +206 -15
  50. package/dist/rbac/index.js +28 -6
  51. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  52. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +114 -3
  56. package/dist/utils.d.ts +110 -152
  57. package/dist/utils.js +128 -138
  58. package/dist/utils.js.map +1 -1
  59. package/docs/api/README.md +60 -1
  60. package/docs/api/classes/ColumnFactory.md +1 -1
  61. package/docs/api/classes/ErrorBoundary.md +1 -1
  62. package/docs/api/classes/InvalidScopeError.md +1 -1
  63. package/docs/api/classes/Logger.md +178 -0
  64. package/docs/api/classes/MissingUserContextError.md +1 -1
  65. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  66. package/docs/api/classes/PermissionDeniedError.md +1 -1
  67. package/docs/api/classes/RBACAuditManager.md +2 -2
  68. package/docs/api/classes/RBACCache.md +1 -1
  69. package/docs/api/classes/RBACEngine.md +2 -2
  70. package/docs/api/classes/RBACError.md +1 -1
  71. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  72. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  73. package/docs/api/classes/StorageUtils.md +1 -1
  74. package/docs/api/enums/FileCategory.md +1 -1
  75. package/docs/api/enums/LogLevel.md +54 -0
  76. package/docs/api/enums/RBACErrorCode.md +1 -1
  77. package/docs/api/enums/RPCFunction.md +1 -1
  78. package/docs/api/interfaces/AggregateConfig.md +1 -1
  79. package/docs/api/interfaces/BadgeProps.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +1 -1
  81. package/docs/api/interfaces/CalendarProps.md +18 -2
  82. package/docs/api/interfaces/CardProps.md +1 -1
  83. package/docs/api/interfaces/ColorPalette.md +1 -1
  84. package/docs/api/interfaces/ColorShade.md +1 -1
  85. package/docs/api/interfaces/ComplianceResult.md +30 -0
  86. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  87. package/docs/api/interfaces/DataRecord.md +1 -1
  88. package/docs/api/interfaces/DataTableAction.md +1 -1
  89. package/docs/api/interfaces/DataTableColumn.md +1 -1
  90. package/docs/api/interfaces/DataTableProps.md +1 -1
  91. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  92. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  93. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  94. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  95. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  97. package/docs/api/interfaces/ExportColumn.md +1 -1
  98. package/docs/api/interfaces/ExportOptions.md +1 -1
  99. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  100. package/docs/api/interfaces/FileMetadata.md +1 -1
  101. package/docs/api/interfaces/FileReference.md +1 -1
  102. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  103. package/docs/api/interfaces/FileUploadOptions.md +48 -8
  104. package/docs/api/interfaces/FileUploadProps.md +46 -13
  105. package/docs/api/interfaces/FooterProps.md +1 -1
  106. package/docs/api/interfaces/FormFieldProps.md +1 -1
  107. package/docs/api/interfaces/FormProps.md +1 -1
  108. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  109. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  110. package/docs/api/interfaces/InputProps.md +1 -1
  111. package/docs/api/interfaces/LabelProps.md +1 -1
  112. package/docs/api/interfaces/LoggerConfig.md +62 -0
  113. package/docs/api/interfaces/LoginFormProps.md +1 -1
  114. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  115. package/docs/api/interfaces/NavigationContextType.md +1 -1
  116. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  117. package/docs/api/interfaces/NavigationItem.md +1 -1
  118. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  119. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  120. package/docs/api/interfaces/Organisation.md +1 -1
  121. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  122. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  123. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  124. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  125. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  126. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  127. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  128. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  129. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  130. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  131. package/docs/api/interfaces/PaletteData.md +1 -1
  132. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  133. package/docs/api/interfaces/ProgressProps.md +1 -1
  134. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  135. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  136. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  138. package/docs/api/interfaces/QuickFix.md +52 -0
  139. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  140. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  141. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  142. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  143. package/docs/api/interfaces/RBACConfig.md +4 -4
  144. package/docs/api/interfaces/RBACContext.md +1 -1
  145. package/docs/api/interfaces/RBACLogger.md +1 -1
  146. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  149. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  151. package/docs/api/interfaces/RBACResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  156. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  157. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  158. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  159. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  160. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  161. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  162. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  163. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  164. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  165. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  166. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  167. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  168. package/docs/api/interfaces/RouteConfig.md +1 -1
  169. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  170. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  171. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  172. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  173. package/docs/api/interfaces/SetupIssue.md +41 -0
  174. package/docs/api/interfaces/StorageConfig.md +1 -1
  175. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  176. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  177. package/docs/api/interfaces/StorageListOptions.md +1 -1
  178. package/docs/api/interfaces/StorageListResult.md +1 -1
  179. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  180. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  181. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  182. package/docs/api/interfaces/StyleImport.md +1 -1
  183. package/docs/api/interfaces/SwitchProps.md +1 -1
  184. package/docs/api/interfaces/TabsContentProps.md +1 -1
  185. package/docs/api/interfaces/TabsListProps.md +1 -1
  186. package/docs/api/interfaces/TabsProps.md +1 -1
  187. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  188. package/docs/api/interfaces/TextareaProps.md +1 -1
  189. package/docs/api/interfaces/ToastActionElement.md +1 -1
  190. package/docs/api/interfaces/ToastProps.md +1 -1
  191. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  192. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  193. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  194. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  195. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  196. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  198. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  202. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  203. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  204. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  205. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  206. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  207. package/docs/api/interfaces/UserEventAccess.md +1 -1
  208. package/docs/api/interfaces/UserMenuProps.md +1 -1
  209. package/docs/api/interfaces/UserProfile.md +1 -1
  210. package/docs/api/modules.md +746 -50
  211. package/docs/api-reference/components.md +26 -12
  212. package/docs/api-reference/hooks.md +111 -0
  213. package/docs/api-reference/rpc-functions.md +1 -1
  214. package/docs/api-reference/utilities.md +184 -0
  215. package/docs/getting-started/installation-guide.md +75 -16
  216. package/docs/getting-started/quick-start.md +61 -11
  217. package/docs/implementation-guides/authentication.md +88 -12
  218. package/docs/implementation-guides/file-reference-system.md +26 -3
  219. package/docs/implementation-guides/file-upload-storage.md +30 -1
  220. package/docs/rbac/README.md +1 -0
  221. package/docs/rbac/compliance/compliance-guide.md +544 -0
  222. package/docs/rbac/getting-started.md +158 -33
  223. package/docs/standards/pace-core-compliance.md +432 -0
  224. package/eslint-config-pace-core.cjs +93 -0
  225. package/package.json +15 -3
  226. package/scripts/analyze-bundle.js +232 -0
  227. package/scripts/build-css.js +56 -0
  228. package/scripts/build-docs-incremental.js +1015 -0
  229. package/scripts/check-pace-core-compliance.cjs +2353 -0
  230. package/scripts/check-pace-core-compliance.js +512 -0
  231. package/scripts/generate-docs.js +157 -0
  232. package/scripts/setup-build-cache.js +73 -0
  233. package/scripts/utils/command-runner.js +131 -0
  234. package/scripts/utils/env.js +33 -0
  235. package/scripts/utils/index.js +10 -0
  236. package/scripts/utils/logger.js +88 -0
  237. package/scripts/utils/path-helpers.js +37 -0
  238. package/scripts/validate-formats.js +133 -0
  239. package/scripts/validate-master.js +155 -0
  240. package/scripts/validate-pre-publish.js +140 -0
  241. package/scripts/validate-theme.js +142 -0
  242. package/src/components/Calendar/Calendar.tsx +8 -1
  243. package/src/components/Card/Card.tsx +47 -8
  244. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  245. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  246. package/src/components/DatePickerWithTimezone/README.md +135 -0
  247. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  248. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  249. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  250. package/src/components/DateTimeField/README.md +148 -0
  251. package/src/components/DateTimeField/index.ts +10 -0
  252. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  253. package/src/components/FileUpload/FileUpload.tsx +10 -1
  254. package/src/components/Header/Header.test.tsx +47 -18
  255. package/src/components/Header/Header.tsx +22 -7
  256. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  257. package/src/components/PaceAppLayout/README.md +9 -0
  258. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  259. package/src/components/ProtectedRoute/ProtectedRoute.tsx +146 -5
  260. package/src/components/index.ts +8 -0
  261. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  262. package/src/eslint-rules/pace-core-compliance.js +640 -0
  263. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  264. package/src/hooks/index.ts +5 -0
  265. package/src/hooks/useFileReference.test.ts +2 -0
  266. package/src/hooks/useFormDialog.ts +147 -0
  267. package/src/hooks/usePreventTabReload.ts +106 -0
  268. package/src/hooks/useSecureDataAccess.ts +2 -2
  269. package/src/index.ts +27 -0
  270. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  271. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  272. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  273. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  274. package/src/rbac/compliance/database-validator.ts +165 -0
  275. package/src/rbac/compliance/index.ts +38 -0
  276. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  277. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  278. package/src/rbac/compliance/setup-validator.ts +131 -0
  279. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  280. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  281. package/src/rbac/docs/event-based-apps.md +285 -0
  282. package/src/rbac/errors.ts +11 -0
  283. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  284. package/src/rbac/index.ts +30 -0
  285. package/src/services/OrganisationService.ts +4 -0
  286. package/src/styles/core.css +5 -5
  287. package/src/types/database.generated.ts +63 -9
  288. package/src/types/file-reference.ts +9 -0
  289. package/src/utils/__tests__/timezone.test.ts +345 -0
  290. package/src/utils/file-reference/__tests__/file-reference.test.ts +60 -4
  291. package/src/utils/file-reference/index.ts +13 -2
  292. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  293. package/src/utils/formatting/formatting.ts +179 -0
  294. package/src/utils/index.ts +27 -1
  295. package/src/utils/location/index.ts +16 -0
  296. package/src/utils/location/location.test.ts +286 -0
  297. package/src/utils/location/location.ts +175 -0
  298. package/src/utils/security/secureDataAccess.ts +1 -1
  299. package/src/utils/storage/helpers.ts +68 -0
  300. package/src/utils/timezone/index.ts +17 -0
  301. package/src/utils/timezone/timezone.test.ts +349 -0
  302. package/src/utils/timezone/timezone.ts +281 -0
  303. package/dist/chunk-CSOFYHAG.js.map +0 -1
  304. package/dist/chunk-FUEYYMX5.js.map +0 -1
  305. package/dist/chunk-HKIT6O7W.js +0 -198
  306. package/dist/chunk-HKIT6O7W.js.map +0 -1
  307. package/dist/chunk-KUEN3HFB.js +0 -94
  308. package/dist/chunk-KUEN3HFB.js.map +0 -1
  309. package/dist/chunk-M7W4CP3M.js.map +0 -1
  310. package/dist/chunk-MI7HBHN3.js.map +0 -1
  311. package/dist/chunk-NQPMQGS2.js.map +0 -1
  312. package/dist/chunk-PWAHJW4G.js.map +0 -1
  313. package/dist/chunk-UHNYIBXL.js.map +0 -1
  314. package/dist/chunk-W22JP75J.js.map +0 -1
  315. package/dist/formatting-5wETwiGF.d.ts +0 -162
  316. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  317. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  318. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  319. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @file Timezone-Aware Date Formatting Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Formatting/__tests__
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for timezone-aware date formatting functions.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import {
12
+ formatDateTimeForDisplay,
13
+ formatDateOnlyForDisplay,
14
+ formatDateTimeForTable,
15
+ formatDateTimeForMap,
16
+ type DateTimeFormatOptions
17
+ } from './formatting';
18
+
19
+ describe('Timezone-Aware Date Formatting', () => {
20
+ describe('formatDateTimeForDisplay', () => {
21
+ it('formats UTC date with timezone abbreviation', () => {
22
+ const result = formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York');
23
+ expect(result).toMatch(/Jan 15, 2024/);
24
+ expect(result).toMatch(/\(/); // Should include timezone abbreviation in parentheses
25
+ });
26
+
27
+ it('formats Date object with timezone', () => {
28
+ const date = new Date('2024-01-15T10:00:00Z');
29
+ const result = formatDateTimeForDisplay(date, 'America/New_York');
30
+ expect(result).toMatch(/Jan 15, 2024/);
31
+ });
32
+
33
+ it('excludes timezone when includeTimezone is false', () => {
34
+ const options: DateTimeFormatOptions = { includeTimezone: false };
35
+ const result = formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York', options);
36
+ expect(result).toMatch(/Jan 15, 2024/);
37
+ expect(result).not.toMatch(/\(/); // Should not include parentheses
38
+ });
39
+
40
+ it('uses custom format string', () => {
41
+ const options: DateTimeFormatOptions = { format: 'yyyy-MM-dd HH:mm' };
42
+ const result = formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York', options);
43
+ expect(result).toMatch(/2024-01-15/);
44
+ });
45
+
46
+ it('returns empty string for undefined date', () => {
47
+ const result = formatDateTimeForDisplay(undefined, 'America/New_York');
48
+ expect(result).toBe('');
49
+ });
50
+
51
+ it('returns empty string for undefined timezone', () => {
52
+ const result = formatDateTimeForDisplay('2024-01-15T10:00:00Z', undefined);
53
+ expect(result).toBe('');
54
+ });
55
+
56
+ it('returns empty string for invalid date', () => {
57
+ const result = formatDateTimeForDisplay('invalid-date', 'America/New_York');
58
+ expect(result).toBe('');
59
+ });
60
+
61
+ it('handles different timezones correctly', () => {
62
+ const date = '2024-01-15T10:00:00Z';
63
+ const ny = formatDateTimeForDisplay(date, 'America/New_York');
64
+ const la = formatDateTimeForDisplay(date, 'America/Los_Angeles');
65
+ expect(ny).not.toBe(la);
66
+ });
67
+ });
68
+
69
+ describe('formatDateOnlyForDisplay', () => {
70
+ it('formats date only without time', () => {
71
+ const result = formatDateOnlyForDisplay('2024-01-15T10:00:00Z');
72
+ expect(result).toMatch(/January/);
73
+ expect(result).toMatch(/2024/);
74
+ expect(result).toMatch(/15/);
75
+ });
76
+
77
+ it('formats Date object', () => {
78
+ const date = new Date('2024-01-15T10:00:00Z');
79
+ const result = formatDateOnlyForDisplay(date);
80
+ expect(result).toMatch(/January/);
81
+ });
82
+
83
+ it('returns empty string for undefined', () => {
84
+ const result = formatDateOnlyForDisplay(undefined);
85
+ expect(result).toBe('');
86
+ });
87
+
88
+ it('returns empty string for invalid date', () => {
89
+ const result = formatDateOnlyForDisplay('invalid-date');
90
+ expect(result).toBe('');
91
+ });
92
+
93
+ it('formats different dates correctly', () => {
94
+ const dates = [
95
+ '2024-01-15T10:00:00Z',
96
+ '2024-06-15T12:00:00Z',
97
+ '2024-12-31T23:59:59Z'
98
+ ];
99
+
100
+ dates.forEach(date => {
101
+ const result = formatDateOnlyForDisplay(date);
102
+ expect(result).toBeTruthy();
103
+ expect(result.length).toBeGreaterThan(0);
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('formatDateTimeForTable', () => {
109
+ it('formats date for table display with timezone', () => {
110
+ const result = formatDateTimeForTable('2024-01-15T10:00:00Z', 'America/New_York');
111
+ expect(result).toMatch(/Jan 15, 2024/);
112
+ expect(result).toMatch(/\(/); // Should include timezone
113
+ });
114
+
115
+ it('returns empty string for undefined date', () => {
116
+ const result = formatDateTimeForTable(undefined, 'America/New_York');
117
+ expect(result).toBe('');
118
+ });
119
+
120
+ it('returns empty string for undefined timezone', () => {
121
+ const result = formatDateTimeForTable('2024-01-15T10:00:00Z', undefined);
122
+ expect(result).toBe('');
123
+ });
124
+
125
+ it('formats Date object', () => {
126
+ const date = new Date('2024-01-15T10:00:00Z');
127
+ const result = formatDateTimeForTable(date, 'America/New_York');
128
+ expect(result).toMatch(/Jan 15, 2024/);
129
+ });
130
+ });
131
+
132
+ describe('formatDateTimeForMap', () => {
133
+ it('formats date for map display', () => {
134
+ const result = formatDateTimeForMap('2024-01-15T10:00:00Z', 'America/New_York');
135
+ expect(result).toMatch(/Jan 15/);
136
+ expect(result).toMatch(/\d{2}:\d{2}/); // Time format
137
+ });
138
+
139
+ it('includes timezone abbreviation', () => {
140
+ const result = formatDateTimeForMap('2024-01-15T10:00:00Z', 'America/New_York');
141
+ expect(result.length).toBeGreaterThan(0);
142
+ // Should include timezone info (not in parentheses for map format)
143
+ });
144
+
145
+ it('returns empty string for undefined date', () => {
146
+ const result = formatDateTimeForMap(undefined, 'America/New_York');
147
+ expect(result).toBe('');
148
+ });
149
+
150
+ it('returns empty string for undefined timezone', () => {
151
+ const result = formatDateTimeForMap('2024-01-15T10:00:00Z', undefined);
152
+ expect(result).toBe('');
153
+ });
154
+
155
+ it('formats Date object', () => {
156
+ const date = new Date('2024-01-15T10:00:00Z');
157
+ const result = formatDateTimeForMap(date, 'America/New_York');
158
+ expect(result).toMatch(/Jan 15/);
159
+ });
160
+
161
+ it('returns empty string for invalid date', () => {
162
+ const result = formatDateTimeForMap('invalid-date', 'America/New_York');
163
+ expect(result).toBe('');
164
+ });
165
+ });
166
+ });
167
+
@@ -2,6 +2,9 @@
2
2
  * Utility functions for formatting data in the application
3
3
  */
4
4
 
5
+ import { parseISO, isValid } from 'date-fns';
6
+ import { formatInTimeZone, getTimezoneAbbreviation } from '../timezone';
7
+
5
8
  /**
6
9
  * Format a date as a readable string in "dd mmm yyyy" format (e.g., "15 Jun 2024")
7
10
  */
@@ -168,3 +171,179 @@ export function formatFileSize(bytes: number): string {
168
171
 
169
172
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
170
173
  }
174
+
175
+ /**
176
+ * Options for formatting date/time with timezone
177
+ */
178
+ export interface DateTimeFormatOptions {
179
+ /**
180
+ * Include timezone abbreviation (default: true)
181
+ */
182
+ includeTimezone?: boolean;
183
+ /**
184
+ * Custom format string (default: 'MMM dd, yyyy HH:mm')
185
+ */
186
+ format?: string;
187
+ }
188
+
189
+ /**
190
+ * Format a UTC date for display in a specific timezone
191
+ *
192
+ * @param utcDate - UTC date (ISO string, Date object, or undefined)
193
+ * @param timezone - IANA timezone string (e.g., 'America/New_York')
194
+ * @param options - Formatting options
195
+ * @returns Formatted date string or empty string if invalid
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York');
200
+ * // "Jan 15, 2024 05:00 (EST)"
201
+ *
202
+ * formatDateTimeForDisplay('2024-01-15T10:00:00Z', 'America/New_York', { includeTimezone: false });
203
+ * // "Jan 15, 2024 05:00"
204
+ * ```
205
+ */
206
+ export function formatDateTimeForDisplay(
207
+ utcDate: string | Date | undefined,
208
+ timezone: string | undefined,
209
+ options: DateTimeFormatOptions = {}
210
+ ): string {
211
+ if (!utcDate) {
212
+ return '';
213
+ }
214
+
215
+ if (!timezone) {
216
+ return '';
217
+ }
218
+
219
+ try {
220
+ const { includeTimezone = true, format: formatStr = 'MMM dd, yyyy HH:mm' } = options;
221
+
222
+ let dateObj: Date;
223
+ if (typeof utcDate === 'string') {
224
+ dateObj = parseISO(utcDate);
225
+ } else {
226
+ dateObj = utcDate;
227
+ }
228
+
229
+ if (!isValid(dateObj)) {
230
+ return '';
231
+ }
232
+
233
+ const formatted = formatInTimeZone(dateObj, timezone, formatStr);
234
+
235
+ if (includeTimezone) {
236
+ const tzAbbr = getTimezoneAbbreviation(dateObj, timezone);
237
+ return `${formatted} (${tzAbbr})`;
238
+ }
239
+
240
+ return formatted;
241
+ } catch {
242
+ return '';
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Format a UTC date for display (date only, no time)
248
+ *
249
+ * @param utcDate - UTC date (ISO string, Date object, or undefined)
250
+ * @returns Formatted date string or empty string if invalid
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * formatDateOnlyForDisplay('2024-01-15T10:00:00Z');
255
+ * // "15 January 2024"
256
+ * ```
257
+ */
258
+ export function formatDateOnlyForDisplay(utcDate: string | Date | undefined): string {
259
+ if (!utcDate) {
260
+ return '';
261
+ }
262
+
263
+ try {
264
+ let dateObj: Date;
265
+ if (typeof utcDate === 'string') {
266
+ dateObj = parseISO(utcDate);
267
+ } else {
268
+ dateObj = utcDate;
269
+ }
270
+
271
+ if (!isValid(dateObj)) {
272
+ return '';
273
+ }
274
+
275
+ // Use 'en-GB' locale for "dd mmm yyyy" format
276
+ return dateObj.toLocaleDateString('en-GB', {
277
+ year: 'numeric',
278
+ month: 'long',
279
+ day: 'numeric'
280
+ });
281
+ } catch {
282
+ return '';
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Format a UTC date for table display (compact format with timezone)
288
+ *
289
+ * @param utcDate - UTC date (ISO string, Date object, or undefined)
290
+ * @param timezone - IANA timezone string
291
+ * @returns Formatted date string or empty string if invalid
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * formatDateTimeForTable('2024-01-15T10:00:00Z', 'America/New_York');
296
+ * // "Jan 15, 2024 05:00 (EST)"
297
+ * ```
298
+ */
299
+ export function formatDateTimeForTable(
300
+ utcDate: string | Date | undefined,
301
+ timezone: string | undefined
302
+ ): string {
303
+ return formatDateTimeForDisplay(utcDate, timezone, {
304
+ includeTimezone: true,
305
+ format: 'MMM dd, yyyy HH:mm'
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Format a UTC date for map display (compact format)
311
+ *
312
+ * @param utcDate - UTC date (ISO string, Date object, or undefined)
313
+ * @param timezone - IANA timezone string
314
+ * @returns Formatted date string or empty string if invalid
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * formatDateTimeForMap('2024-01-15T10:00:00Z', 'America/New_York');
319
+ * // "Jan 15, 05:00 EST"
320
+ * ```
321
+ */
322
+ export function formatDateTimeForMap(
323
+ utcDate: string | Date | undefined,
324
+ timezone: string | undefined
325
+ ): string {
326
+ if (!utcDate || !timezone) {
327
+ return '';
328
+ }
329
+
330
+ try {
331
+ let dateObj: Date;
332
+ if (typeof utcDate === 'string') {
333
+ dateObj = parseISO(utcDate);
334
+ } else {
335
+ dateObj = utcDate;
336
+ }
337
+
338
+ if (!isValid(dateObj)) {
339
+ return '';
340
+ }
341
+
342
+ const formatted = formatInTimeZone(dateObj, timezone, 'MMM dd, HH:mm');
343
+ const tzAbbr = getTimezoneAbbreviation(dateObj, timezone);
344
+
345
+ return `${formatted} ${tzAbbr}`;
346
+ } catch {
347
+ return '';
348
+ }
349
+ }
@@ -120,8 +120,13 @@ export {
120
120
  formatNumber,
121
121
  formatPercent,
122
122
  formatCompactNumber,
123
- formatFileSize
123
+ formatFileSize,
124
+ formatDateTimeForDisplay,
125
+ formatDateOnlyForDisplay,
126
+ formatDateTimeForTable,
127
+ formatDateTimeForMap
124
128
  } from './formatting/formatting';
129
+ export type { DateTimeFormatOptions } from './formatting/formatting';
125
130
 
126
131
  // Organisation context utilities
127
132
  export {
@@ -130,3 +135,24 @@ export {
130
135
  getOrganisationContext,
131
136
  isOrganisationContextAvailable
132
137
  } from './context/organisationContext';
138
+
139
+ // Timezone utilities
140
+ export {
141
+ formatInTimeZone,
142
+ getTimezoneAbbreviation,
143
+ formatTimeInTimeZone,
144
+ getUserTimeZone,
145
+ toZonedTime,
146
+ fromZonedTime,
147
+ roundToNearestMinutes,
148
+ getTimeZoneDifference
149
+ } from './timezone';
150
+
151
+ // Location utilities
152
+ export {
153
+ formatCoordinates,
154
+ hasValidCoordinates,
155
+ areCoordinatesEqual,
156
+ getGoogleMapsUrl
157
+ } from './location';
158
+ export type { Coordinates } from './location';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file Location Utilities Exports
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Location
5
+ * @since 0.1.0
6
+ */
7
+
8
+ export {
9
+ formatCoordinates,
10
+ hasValidCoordinates,
11
+ areCoordinatesEqual,
12
+ getGoogleMapsUrl
13
+ } from './location';
14
+
15
+ export type { Coordinates } from './location';
16
+
@@ -0,0 +1,286 @@
1
+ /**
2
+ * @file Location Utilities Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/Location/__tests__
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for location utility functions.
8
+ * Tests cover all major functionality, edge cases, and error handling.
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import {
13
+ formatCoordinates,
14
+ hasValidCoordinates,
15
+ areCoordinatesEqual,
16
+ getGoogleMapsUrl,
17
+ type Coordinates
18
+ } from './location';
19
+
20
+ describe('Location Utilities', () => {
21
+ describe('formatCoordinates', () => {
22
+ it('formats valid coordinates with 6 decimal places', () => {
23
+ const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
24
+ const result = formatCoordinates(coords);
25
+ expect(result).toBe('-37.813600, 144.963100');
26
+ });
27
+
28
+ it('formats coordinates with fewer decimal places', () => {
29
+ const coords: Coordinates = { lat: 0, lng: 0 };
30
+ const result = formatCoordinates(coords);
31
+ expect(result).toBe('0.000000, 0.000000');
32
+ });
33
+
34
+ it('formats coordinates with many decimal places', () => {
35
+ const coords: Coordinates = { lat: -37.813612345, lng: 144.963198765 };
36
+ const result = formatCoordinates(coords);
37
+ expect(result).toBe('-37.813612, 144.963199');
38
+ });
39
+
40
+ it('returns "N/A" for undefined', () => {
41
+ const result = formatCoordinates(undefined);
42
+ expect(result).toBe('N/A');
43
+ });
44
+
45
+ it('returns "N/A" for null', () => {
46
+ // @ts-expect-error - Testing null input
47
+ const result = formatCoordinates(null);
48
+ expect(result).toBe('N/A');
49
+ });
50
+
51
+ it('returns "N/A" for missing lat', () => {
52
+ // @ts-expect-error - Testing invalid input
53
+ const result = formatCoordinates({ lng: 144.9631 });
54
+ expect(result).toBe('N/A');
55
+ });
56
+
57
+ it('returns "N/A" for missing lng', () => {
58
+ // @ts-expect-error - Testing invalid input
59
+ const result = formatCoordinates({ lat: -37.8136 });
60
+ expect(result).toBe('N/A');
61
+ });
62
+
63
+ it('returns "N/A" for NaN values', () => {
64
+ const coords: Coordinates = { lat: NaN, lng: 144.9631 };
65
+ const result = formatCoordinates(coords);
66
+ expect(result).toBe('N/A');
67
+ });
68
+
69
+ it('returns "N/A" for Infinity values', () => {
70
+ const coords: Coordinates = { lat: Infinity, lng: 144.9631 };
71
+ const result = formatCoordinates(coords);
72
+ expect(result).toBe('N/A');
73
+ });
74
+
75
+ it('returns "N/A" for non-number lat', () => {
76
+ // @ts-expect-error - Testing invalid input
77
+ const result = formatCoordinates({ lat: 'invalid', lng: 144.9631 });
78
+ expect(result).toBe('N/A');
79
+ });
80
+
81
+ it('returns "N/A" for non-number lng', () => {
82
+ // @ts-expect-error - Testing invalid input
83
+ const result = formatCoordinates({ lat: -37.8136, lng: 'invalid' });
84
+ expect(result).toBe('N/A');
85
+ });
86
+ });
87
+
88
+ describe('hasValidCoordinates', () => {
89
+ it('returns true for valid coordinates', () => {
90
+ const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
91
+ expect(hasValidCoordinates(coords)).toBe(true);
92
+ });
93
+
94
+ it('returns true for coordinates at boundaries', () => {
95
+ expect(hasValidCoordinates({ lat: -90, lng: -180 })).toBe(true);
96
+ expect(hasValidCoordinates({ lat: 90, lng: 180 })).toBe(true);
97
+ expect(hasValidCoordinates({ lat: 0, lng: 0 })).toBe(true);
98
+ });
99
+
100
+ it('returns false for lat out of range (too low)', () => {
101
+ expect(hasValidCoordinates({ lat: -91, lng: 0 })).toBe(false);
102
+ });
103
+
104
+ it('returns false for lat out of range (too high)', () => {
105
+ expect(hasValidCoordinates({ lat: 91, lng: 0 })).toBe(false);
106
+ });
107
+
108
+ it('returns false for lng out of range (too low)', () => {
109
+ expect(hasValidCoordinates({ lat: 0, lng: -181 })).toBe(false);
110
+ });
111
+
112
+ it('returns false for lng out of range (too high)', () => {
113
+ expect(hasValidCoordinates({ lat: 0, lng: 181 })).toBe(false);
114
+ });
115
+
116
+ it('returns false for undefined', () => {
117
+ expect(hasValidCoordinates(undefined)).toBe(false);
118
+ });
119
+
120
+ it('returns false for null', () => {
121
+ // @ts-expect-error - Testing null input
122
+ expect(hasValidCoordinates(null)).toBe(false);
123
+ });
124
+
125
+ it('returns false for missing lat', () => {
126
+ expect(hasValidCoordinates({ lng: 144.9631 })).toBe(false);
127
+ });
128
+
129
+ it('returns false for missing lng', () => {
130
+ expect(hasValidCoordinates({ lat: -37.8136 })).toBe(false);
131
+ });
132
+
133
+ it('returns false for NaN lat', () => {
134
+ expect(hasValidCoordinates({ lat: NaN, lng: 144.9631 })).toBe(false);
135
+ });
136
+
137
+ it('returns false for NaN lng', () => {
138
+ expect(hasValidCoordinates({ lat: -37.8136, lng: NaN })).toBe(false);
139
+ });
140
+
141
+ it('returns false for Infinity lat', () => {
142
+ expect(hasValidCoordinates({ lat: Infinity, lng: 144.9631 })).toBe(false);
143
+ });
144
+
145
+ it('returns false for Infinity lng', () => {
146
+ expect(hasValidCoordinates({ lat: -37.8136, lng: Infinity })).toBe(false);
147
+ });
148
+
149
+ it('returns false for non-number lat', () => {
150
+ // @ts-expect-error - Testing invalid input
151
+ expect(hasValidCoordinates({ lat: 'invalid', lng: 144.9631 })).toBe(false);
152
+ });
153
+
154
+ it('returns false for non-number lng', () => {
155
+ // @ts-expect-error - Testing invalid input
156
+ expect(hasValidCoordinates({ lat: -37.8136, lng: 'invalid' })).toBe(false);
157
+ });
158
+ });
159
+
160
+ describe('areCoordinatesEqual', () => {
161
+ it('returns true for identical coordinates', () => {
162
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
163
+ const coords2: Coordinates = { lat: -37.8136, lng: 144.9631 };
164
+ expect(areCoordinatesEqual(coords1, coords2)).toBe(true);
165
+ });
166
+
167
+ it('returns true for coordinates within default tolerance', () => {
168
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
169
+ const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
170
+ expect(areCoordinatesEqual(coords1, coords2)).toBe(true);
171
+ });
172
+
173
+ it('returns false for coordinates outside default tolerance', () => {
174
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
175
+ const coords2: Coordinates = { lat: -37.8150, lng: 144.9650 };
176
+ expect(areCoordinatesEqual(coords1, coords2)).toBe(false);
177
+ });
178
+
179
+ it('returns true for coordinates within custom tolerance', () => {
180
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
181
+ const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
182
+ expect(areCoordinatesEqual(coords1, coords2, 0.001)).toBe(true);
183
+ });
184
+
185
+ it('returns false for coordinates outside custom tolerance', () => {
186
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
187
+ const coords2: Coordinates = { lat: -37.8137, lng: 144.9632 };
188
+ expect(areCoordinatesEqual(coords1, coords2, 0.00001)).toBe(false);
189
+ });
190
+
191
+ it('returns true when both are null', () => {
192
+ expect(areCoordinatesEqual(null, null)).toBe(true);
193
+ });
194
+
195
+ it('returns true when both are undefined', () => {
196
+ expect(areCoordinatesEqual(undefined, undefined)).toBe(true);
197
+ });
198
+
199
+ it('returns false when one is null and other is valid', () => {
200
+ const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
201
+ expect(areCoordinatesEqual(null, coords)).toBe(false);
202
+ expect(areCoordinatesEqual(coords, null)).toBe(false);
203
+ });
204
+
205
+ it('returns false when one is undefined and other is valid', () => {
206
+ const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
207
+ expect(areCoordinatesEqual(undefined, coords)).toBe(false);
208
+ expect(areCoordinatesEqual(coords, undefined)).toBe(false);
209
+ });
210
+
211
+ it('returns false for invalid coordinates', () => {
212
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
213
+ // @ts-expect-error - Testing invalid input
214
+ const coords2 = { lat: 91, lng: 0 };
215
+ expect(areCoordinatesEqual(coords1, coords2)).toBe(false);
216
+ });
217
+
218
+ it('handles edge case: exactly at tolerance boundary', () => {
219
+ const coords1: Coordinates = { lat: -37.8136, lng: 144.9631 };
220
+ const coords2: Coordinates = { lat: -37.8136 + 0.0001, lng: 144.9631 };
221
+ expect(areCoordinatesEqual(coords1, coords2, 0.0001)).toBe(true);
222
+ });
223
+ });
224
+
225
+ describe('getGoogleMapsUrl', () => {
226
+ it('generates URL for valid coordinates', () => {
227
+ const coords: Coordinates = { lat: -37.8136, lng: 144.9631 };
228
+ const result = getGoogleMapsUrl(coords);
229
+ expect(result).toBe('https://www.google.com/maps/search/?api=1&query=-37.8136,144.9631');
230
+ });
231
+
232
+ it('generates URL with negative coordinates', () => {
233
+ const coords: Coordinates = { lat: -90, lng: -180 };
234
+ const result = getGoogleMapsUrl(coords);
235
+ expect(result).toBe('https://www.google.com/maps/search/?api=1&query=-90,-180');
236
+ });
237
+
238
+ it('generates URL with positive coordinates', () => {
239
+ const coords: Coordinates = { lat: 90, lng: 180 };
240
+ const result = getGoogleMapsUrl(coords);
241
+ expect(result).toBe('https://www.google.com/maps/search/?api=1&query=90,180');
242
+ });
243
+
244
+ it('returns empty string for undefined', () => {
245
+ const result = getGoogleMapsUrl(undefined);
246
+ expect(result).toBe('');
247
+ });
248
+
249
+ it('returns empty string for null', () => {
250
+ // @ts-expect-error - Testing null input
251
+ const result = getGoogleMapsUrl(null);
252
+ expect(result).toBe('');
253
+ });
254
+
255
+ it('returns empty string for invalid coordinates', () => {
256
+ // @ts-expect-error - Testing invalid input
257
+ const result = getGoogleMapsUrl({ lat: 91, lng: 0 });
258
+ expect(result).toBe('');
259
+ });
260
+
261
+ it('returns empty string for missing lat', () => {
262
+ // @ts-expect-error - Testing invalid input
263
+ const result = getGoogleMapsUrl({ lng: 144.9631 });
264
+ expect(result).toBe('');
265
+ });
266
+
267
+ it('returns empty string for missing lng', () => {
268
+ // @ts-expect-error - Testing invalid input
269
+ const result = getGoogleMapsUrl({ lat: -37.8136 });
270
+ expect(result).toBe('');
271
+ });
272
+
273
+ it('returns empty string for NaN values', () => {
274
+ const coords: Coordinates = { lat: NaN, lng: 144.9631 };
275
+ const result = getGoogleMapsUrl(coords);
276
+ expect(result).toBe('');
277
+ });
278
+
279
+ it('returns empty string for Infinity values', () => {
280
+ const coords: Coordinates = { lat: Infinity, lng: 144.9631 };
281
+ const result = getGoogleMapsUrl(coords);
282
+ expect(result).toBe('');
283
+ });
284
+ });
285
+ });
286
+