@jmruthers/pace-core 0.6.8 → 0.6.10

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 (669) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/audit-tool/audits/02-project-structure.cjs +97 -32
  3. package/audit-tool/audits/03-architecture.cjs +145 -19
  4. package/audit-tool/audits/04-code-quality.cjs +86 -1
  5. package/audit-tool/audits/06-security-rbac.cjs +109 -11
  6. package/cursor-rules/02-project-structure.mdc +2 -26
  7. package/cursor-rules/05-styling.mdc +84 -6
  8. package/cursor-rules/06-security-rbac.mdc +124 -1
  9. package/dist/{DataTable-6RMSCQJ6.js → DataTable-SAXFG4XI.js} +11 -13
  10. package/dist/{AuthService-DmfO5rGS.d.ts → InactivityServiceProvider-DHryoh6K.d.ts} +24 -249
  11. package/dist/UnifiedAuthProvider-BBD2PS3Q.js +7 -0
  12. package/dist/{UnifiedAuthProvider-CKvHP1MK.d.ts → UnifiedAuthProvider-CiBAl9-s.d.ts} +34 -22
  13. package/dist/{api-7P7DI652.js → api-F47QJ7FX.js} +3 -3
  14. package/dist/assets/app-icons/admin_favicon.svg +462 -0
  15. package/dist/assets/app-icons/base_favicon.svg +85 -0
  16. package/dist/assets/app-icons/cake_favicon.svg +68 -0
  17. package/dist/assets/app-icons/core_favicon.svg +256 -0
  18. package/dist/assets/app-icons/gear_favicon.svg +91 -0
  19. package/dist/assets/app-icons/medi_favicon.svg +92 -0
  20. package/dist/assets/app-icons/mint_favicon.svg +83 -0
  21. package/dist/assets/app-icons/pace_favicon.svg +49 -0
  22. package/dist/assets/app-icons/pump_favicon.svg +68 -0
  23. package/dist/assets/app-icons/seed_favicon.svg +91 -0
  24. package/dist/assets/app-icons/team_favicon.svg +67 -0
  25. package/dist/assets/app-icons/trac_favicon.svg +112 -0
  26. package/dist/assets/app-icons/trip_favicon.svg +102 -0
  27. package/dist/audit-Z6ZZBWLU.js +3 -0
  28. package/dist/chunk-3GWSPISD.js +61 -0
  29. package/dist/{chunk-4DDCYDQ3.js → chunk-66R6RLUZ.js} +12 -27
  30. package/dist/{chunk-FYHN4DD5.js → chunk-7YDC7LMU.js} +80 -8
  31. package/dist/{chunk-S7DKJPLT.js → chunk-BCTXBU6U.js} +22 -17
  32. package/dist/{chunk-TTRFSOKR.js → chunk-BTHN5MKC.js} +4 -4
  33. package/dist/{chunk-A3W6LW53.js → chunk-DDMPHZ3D.js} +6 -18
  34. package/dist/{chunk-MPBLMWVR.js → chunk-FBZ7U3ID.js} +140 -92
  35. package/dist/chunk-FN52B75D.js +246 -0
  36. package/dist/{chunk-5W2A3DRC.js → chunk-JJEYZ3DX.js} +5 -4
  37. package/dist/chunk-KPYQWGFQ.js +183 -0
  38. package/dist/{chunk-IUBRCBSY.js → chunk-KSNLMI7N.js} +14 -8
  39. package/dist/chunk-KYURMOQM.js +977 -0
  40. package/dist/{chunk-LX6U42O3.js → chunk-LNHFAF4X.js} +160 -58
  41. package/dist/{chunk-NKHKXPI4.js → chunk-MPY44PWB.js} +683 -627
  42. package/dist/{chunk-AHU7G2R5.js → chunk-NIU6DPQV.js} +10 -6
  43. package/dist/{chunk-HF6O3O37.js → chunk-RMLY6KB5.js} +1 -1
  44. package/dist/{chunk-6GLLNA6U.js → chunk-SACF5YSM.js} +1 -1
  45. package/dist/{chunk-EURB7QFZ.js → chunk-TFIPNIPE.js} +867 -534
  46. package/dist/{chunk-OJ4SKRSV.js → chunk-UZNAFKGW.js} +25 -5
  47. package/dist/chunk-W46INAVW.js +1216 -0
  48. package/dist/chunk-X5EAU5G7.js +793 -0
  49. package/dist/{chunk-T5CVK4R3.js → chunk-Y4PF6HIM.js} +110 -64
  50. package/dist/components.d.ts +8 -86
  51. package/dist/components.js +21 -55
  52. package/dist/{database.generated-CcnC_DRc.d.ts → database.generated-DT8JTZiP.d.ts} +12 -12
  53. package/dist/eslint-rules/rules/05-styling.cjs +507 -0
  54. package/dist/eslint-rules/rules/06-security-rbac.cjs +10 -0
  55. package/dist/{event-CW5YB_2p.d.ts → event-WTAQuGcq.d.ts} +1 -1
  56. package/dist/{functions-lBy5L2ry.d.ts → functions-DH45k8ec.d.ts} +1 -1
  57. package/dist/hooks.d.ts +12 -11
  58. package/dist/hooks.js +69 -44
  59. package/dist/index.d.ts +380 -32
  60. package/dist/index.js +46 -32
  61. package/dist/papaparseLoader-WG2UXQ22.js +7 -0
  62. package/dist/providers.d.ts +28 -14
  63. package/dist/providers.js +5 -5
  64. package/dist/rbac/eslint-rules.js +2 -2
  65. package/dist/rbac/index.d.ts +58 -214
  66. package/dist/rbac/index.js +11 -11
  67. package/dist/theming/runtime.d.ts +9 -3
  68. package/dist/theming/runtime.js +2 -2
  69. package/dist/{timezone-BZe_eUxx.d.ts → timezone-K-ptz3HO.d.ts} +22 -23
  70. package/dist/{types-t9H8qKRw.d.ts → types-BE2sEHKd.d.ts} +1 -1
  71. package/dist/{types-BeoeWV5I.d.ts → types-CvOPXWWZ.d.ts} +6 -5
  72. package/dist/{types-DXstZpNI.d.ts → types-D05dCGma.d.ts} +56 -149
  73. package/dist/types-Dr8sNhER.d.ts +50 -0
  74. package/dist/types.d.ts +5 -5
  75. package/dist/{PublicPageProvider-CIGSujI2.d.ts → usePublicPageContext-vxBlEHO9.d.ts} +294 -151
  76. package/dist/{usePublicRouteParams-MamNgwqe.d.ts → usePublicRouteParams-G3Ks53mk.d.ts} +8 -7
  77. package/dist/utils.d.ts +301 -137
  78. package/dist/utils.js +42 -41
  79. package/dist/{validation-643vUDZW.d.ts → validation-g5n0hDkh.d.ts} +2 -2
  80. package/docs/api/modules.md +542 -549
  81. package/docs/api-reference/components.md +5 -5
  82. package/docs/api-reference/rpc-functions.md +3 -3
  83. package/docs/implementation-guides/data-tables.md +256 -8
  84. package/docs/rbac/RBAC_CONTRACT.md +0 -12
  85. package/docs/standards/2-project-structure-standards.md +12 -74
  86. package/docs/standards/6-security-rbac-standards.md +222 -7
  87. package/docs/standards/7-api-tech-stack-standards.md +91 -3
  88. package/docs/testing/README.md +10 -0
  89. package/docs/testing/test-setup-for-consumers.md +914 -0
  90. package/eslint-config-pace-core.cjs +4 -0
  91. package/package.json +1 -1
  92. package/scripts/eslint-audit.cjs +110 -11
  93. package/src/__mocks__/lucide-react.ts +0 -2
  94. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +0 -2
  95. package/src/__tests__/index.test.ts +532 -0
  96. package/src/__tests__/integration/UserProfile.test.tsx +1 -1
  97. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +10 -8
  98. package/src/__tests__/rls-policies.test.ts +3 -2
  99. package/src/assets/app-icons/admin_favicon.svg +462 -0
  100. package/src/assets/app-icons/base_favicon.svg +85 -0
  101. package/src/assets/app-icons/cake_favicon.svg +68 -0
  102. package/src/assets/app-icons/core_favicon.svg +256 -0
  103. package/src/assets/app-icons/gear_favicon.svg +91 -0
  104. package/src/assets/app-icons/index.ts +83 -0
  105. package/src/assets/app-icons/medi_favicon.svg +92 -0
  106. package/src/assets/app-icons/mint_favicon.svg +83 -0
  107. package/src/assets/app-icons/pace_favicon.svg +49 -0
  108. package/src/assets/app-icons/pump_favicon.svg +68 -0
  109. package/src/assets/app-icons/seed_favicon.svg +91 -0
  110. package/src/assets/app-icons/team_favicon.svg +67 -0
  111. package/src/assets/app-icons/trac_favicon.svg +112 -0
  112. package/src/assets/app-icons/trip_favicon.svg +102 -0
  113. package/src/components/AddressField/AddressField.test.tsx +378 -3
  114. package/src/components/AddressField/AddressField.tsx +2 -2
  115. package/src/components/AddressField/types.ts +2 -2
  116. package/src/components/Alert/Alert.test.tsx +35 -25
  117. package/src/components/Alert/Alert.tsx +8 -8
  118. package/src/components/AppSwitcher/AppSwitcher.test.tsx +1250 -0
  119. package/src/components/AppSwitcher/AppSwitcher.tsx +315 -0
  120. package/src/components/Avatar/Avatar.test.tsx +11 -1
  121. package/src/components/Avatar/Avatar.tsx +3 -2
  122. package/src/components/Badge/Badge.test.tsx +11 -1
  123. package/src/components/Button/Button.test.tsx +13 -3
  124. package/src/components/Calendar/Calendar.test.tsx +523 -131
  125. package/src/components/Calendar/Calendar.tsx +107 -488
  126. package/src/components/Card/Card.test.tsx +220 -249
  127. package/src/components/Checkbox/Checkbox.test.tsx +58 -174
  128. package/src/components/ContextSelector/ContextSelector.tsx +3 -3
  129. package/src/components/ContextSelector/__tests__/ContextSelector.test.tsx +360 -0
  130. package/src/components/DataTable/DataTable.tsx +2 -2
  131. package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +1 -1
  132. package/src/components/DataTable/__tests__/DataTable.export.test.tsx +2 -2
  133. package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +1 -1
  134. package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +485 -0
  135. package/src/components/DataTable/__tests__/DataTable.test.tsx +2 -2
  136. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +1 -1
  137. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +76 -580
  138. package/src/components/DataTable/__tests__/README.md +1 -1
  139. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +1 -1
  140. package/src/components/DataTable/__tests__/keyboard.test.tsx +1 -1
  141. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +1 -3
  142. package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +0 -6
  143. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +14 -6
  144. package/src/components/DataTable/components/ActionButtons.tsx +9 -4
  145. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +3 -3
  146. package/src/components/DataTable/components/ColumnFilter.tsx +2 -7
  147. package/src/components/DataTable/components/DataTableCore.tsx +44 -52
  148. package/src/components/DataTable/components/DataTableLayout.tsx +37 -26
  149. package/src/components/DataTable/components/DataTableModals.tsx +118 -30
  150. package/src/components/DataTable/components/DataTableToolbar.tsx +2 -2
  151. package/src/components/DataTable/components/EditFields.tsx +6 -47
  152. package/src/components/DataTable/components/EditableRow.tsx +8 -8
  153. package/src/components/DataTable/components/EmptyState.tsx +6 -3
  154. package/src/components/DataTable/components/FilterRow.tsx +18 -11
  155. package/src/components/DataTable/components/GroupingDropdown.tsx +0 -1
  156. package/src/components/DataTable/components/ImportModal.tsx +305 -133
  157. package/src/components/DataTable/components/LoadingState.tsx +2 -2
  158. package/src/components/DataTable/components/PaginationControls.tsx +0 -4
  159. package/src/components/DataTable/components/RowComponent.tsx +42 -22
  160. package/src/components/DataTable/components/UnifiedTableBody.tsx +52 -12
  161. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +51 -463
  162. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +122 -116
  163. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +40 -68
  164. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +9 -137
  165. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +57 -17
  166. package/src/components/DataTable/components/__tests__/DataTableCore.test.tsx +792 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +24 -65
  168. package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +467 -0
  169. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +8 -125
  170. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +528 -56
  171. package/src/components/DataTable/components/__tests__/EditFields.test.tsx +526 -0
  172. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +1 -68
  173. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +8 -25
  174. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +3 -62
  175. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +9 -14
  176. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +50 -186
  177. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +39 -97
  178. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +13 -103
  179. package/src/components/DataTable/components/__tests__/RowComponent.test.tsx +629 -0
  180. package/src/components/DataTable/components/__tests__/SortIndicator.test.tsx +135 -0
  181. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +31 -171
  182. package/src/components/DataTable/components/__tests__/cellValueUtils.test.ts +453 -0
  183. package/src/components/DataTable/components/hooks/useImportModalFocus.test.ts +184 -0
  184. package/src/components/DataTable/components/hooks/usePermissionTracking.test.ts +381 -0
  185. package/src/components/DataTable/context/DataTableContext.tsx +9 -10
  186. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +12 -26
  187. package/src/components/DataTable/core/ColumnFactory.ts +3 -3
  188. package/src/components/DataTable/core/ColumnManager.ts +0 -1
  189. package/src/components/DataTable/core/DataManager.ts +4 -2
  190. package/src/components/DataTable/core/LocalDataAdapter.ts +1 -1
  191. package/src/components/DataTable/core/PluginRegistry.ts +2 -2
  192. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +114 -2
  193. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +103 -5
  194. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +57 -0
  195. package/src/components/DataTable/core/__tests__/DataManager.test.ts +63 -0
  196. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +42 -9
  197. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +29 -7
  198. package/src/components/DataTable/core/__tests__/StateManager.test.ts +58 -4
  199. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +16 -21
  200. package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +93 -4
  201. package/src/components/DataTable/hooks/__tests__/useDataTableConfiguration.test.ts +227 -54
  202. package/src/components/DataTable/hooks/__tests__/useDataTableDataPipeline.test.ts +215 -62
  203. package/src/components/DataTable/hooks/__tests__/useDataTablePermissions.test.ts +217 -39
  204. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +101 -6
  205. package/src/components/DataTable/hooks/__tests__/useEffectiveColumnOrder.test.ts +157 -27
  206. package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +80 -0
  207. package/src/components/DataTable/hooks/__tests__/useKeyboardNavigation.test.ts +787 -0
  208. package/src/components/DataTable/hooks/__tests__/useServerSideDataEffect.test.ts +258 -0
  209. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +298 -23
  210. package/src/components/DataTable/hooks/__tests__/useTableHandlers.test.ts +440 -0
  211. package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +12 -9
  212. package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +12 -9
  213. package/src/components/DataTable/hooks/useDataTableConfiguration.ts +1 -1
  214. package/src/components/DataTable/hooks/useDataTablePermissions.ts +11 -22
  215. package/src/components/DataTable/hooks/useDataTableState.ts +20 -24
  216. package/src/components/DataTable/hooks/useKeyboardNavigation.ts +5 -5
  217. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +13 -1
  218. package/src/components/DataTable/hooks/useTableColumns.ts +36 -38
  219. package/src/components/DataTable/hooks/useTableHandlers.ts +8 -20
  220. package/src/components/DataTable/index.ts +24 -2
  221. package/src/components/DataTable/types.ts +6 -3
  222. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +3 -67
  223. package/src/components/DataTable/utils/__tests__/aggregationUtils.test.ts +288 -0
  224. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +3 -60
  225. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +1 -1
  226. package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +9 -21
  227. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +102 -86
  228. package/src/components/DataTable/utils/__tests__/paginationUtils.test.ts +593 -0
  229. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +33 -49
  230. package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +208 -0
  231. package/src/components/DataTable/utils/a11yUtils.ts +1 -1
  232. package/src/components/DataTable/utils/aggregationUtils.ts +5 -5
  233. package/src/components/DataTable/utils/errorHandling.ts +3 -1
  234. package/src/components/DataTable/utils/exportUtils.ts +1 -1
  235. package/src/components/DataTable/utils/flexibleImport.ts +2 -2
  236. package/src/components/DataTable/utils/hierarchicalSorting.ts +3 -3
  237. package/src/components/DataTable/utils/paginationUtils.ts +1 -1
  238. package/src/components/DataTable/utils/performanceUtils.ts +1 -1
  239. package/src/components/DataTable/utils/selectFieldUtils.ts +127 -0
  240. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +17 -24
  241. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +1 -1
  242. package/src/components/DateTimeField/DateTimeField.test.tsx +2 -15
  243. package/src/components/DateTimeField/DateTimeField.tsx +1 -1
  244. package/src/components/Dialog/Dialog.test.tsx +2007 -407
  245. package/src/components/Dialog/Dialog.tsx +97 -192
  246. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +2 -62
  247. package/src/components/ErrorBoundary/ErrorBoundaryContext.context.ts +17 -0
  248. package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +2 -45
  249. package/src/components/ErrorBoundary/ErrorBoundaryContext.types.ts +41 -0
  250. package/src/components/ErrorBoundary/index.ts +3 -4
  251. package/src/components/ErrorBoundary/useErrorBoundaryContext.ts +20 -0
  252. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -222
  253. package/src/components/FileDisplay/FileDisplay.tsx +14 -12
  254. package/src/components/FileDisplay/index.tsx +1 -1
  255. package/src/components/FileUpload/FileUpload.test.tsx +54 -18
  256. package/src/components/FileUpload/FileUpload.tsx +10 -7
  257. package/src/components/FileUpload/index.tsx +1 -1
  258. package/src/components/Footer/Footer.test.tsx +33 -114
  259. package/src/components/Form/Form.test.tsx +388 -68
  260. package/src/components/Form/Form.tsx +57 -42
  261. package/src/components/Header/Header.test.tsx +645 -154
  262. package/src/components/Header/Header.tsx +52 -43
  263. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +35 -76
  264. package/src/components/Input/Input.test.tsx +34 -120
  265. package/src/components/Label/Label.test.tsx +47 -46
  266. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +9 -12
  267. package/src/components/LoginForm/LoginForm.test.tsx +0 -1
  268. package/src/components/NavigationMenu/NavigationMenu.test.tsx +1399 -82
  269. package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
  270. package/src/components/NavigationMenu/__tests__/useNavigationFiltering.test.ts +1934 -0
  271. package/src/components/NavigationMenu/useNavigationFiltering.ts +5 -15
  272. package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +1307 -0
  273. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +47 -46
  274. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +81 -38
  275. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +87 -66
  276. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +245 -39
  277. package/src/components/PaceAppLayout/PaceAppLayout.tsx +14 -20
  278. package/src/components/PaceAppLayout/README.md +0 -9
  279. package/src/components/PaceAppLayout/test-setup.tsx +15 -9
  280. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +759 -3
  281. package/src/components/PaceLoginPage/PaceLoginPage.tsx +2 -3
  282. package/src/components/PasswordChange/PasswordChangeForm.test.tsx +1 -1
  283. package/src/components/Progress/Progress.test.tsx +127 -1
  284. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +1196 -4
  285. package/src/components/ProtectedRoute/ProtectedRoute.tsx +24 -6
  286. package/src/components/PublicLayout/PublicLayout.test.tsx +1435 -14
  287. package/src/components/PublicLayout/PublicPageContext.ts +28 -0
  288. package/src/components/PublicLayout/PublicPageLayout.tsx +6 -6
  289. package/src/components/PublicLayout/PublicPageProvider.tsx +2 -41
  290. package/src/components/PublicLayout/usePublicPageContext.ts +36 -0
  291. package/src/components/Select/Select.test.tsx +46 -9
  292. package/src/components/Select/Select.tsx +31 -24
  293. package/src/components/Select/__tests__/context.test.tsx +56 -0
  294. package/src/components/Select/hooks/__tests__/useSelectEvents.test.ts +279 -0
  295. package/src/components/Select/hooks/__tests__/useSelectSearch.test.tsx +295 -0
  296. package/src/components/Select/hooks/__tests__/useSelectState.test.ts +254 -0
  297. package/src/components/Select/hooks/useSelectState.ts +16 -16
  298. package/src/components/Select/types.ts +3 -0
  299. package/src/components/Select/utils/__tests__/text.test.tsx +104 -0
  300. package/src/components/SessionRestorationLoader/SessionRestorationLoader.test.tsx +28 -112
  301. package/src/components/Switch/Switch.test.tsx +57 -153
  302. package/src/components/Table/Table.test.tsx +47 -317
  303. package/src/components/Tabs/Tabs.tsx +3 -3
  304. package/src/components/Textarea/Textarea.test.tsx +11 -38
  305. package/src/components/Toast/Toast.test.tsx +78 -569
  306. package/src/components/Tooltip/Tooltip.test.tsx +4 -21
  307. package/src/components/UserMenu/UserMenu.test.tsx +1 -21
  308. package/src/components/UserMenu/UserMenu.tsx +3 -6
  309. package/src/components/__tests__/index.test.ts +346 -0
  310. package/src/components/index.ts +12 -1
  311. package/src/constants/__tests__/performance.test.ts +91 -0
  312. package/src/hooks/__tests__/ServiceHooks.test.tsx +239 -129
  313. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -3
  314. package/src/hooks/__tests__/useApiFetch.unit.test.ts +1 -1
  315. package/src/hooks/__tests__/useAppConfig.unit.test.ts +88 -29
  316. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +282 -98
  317. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +53 -109
  318. package/src/hooks/__tests__/useDataTableState.test.ts +143 -49
  319. package/src/hooks/__tests__/useDebounce.unit.test.ts +94 -19
  320. package/src/hooks/__tests__/useEvents.unit.test.ts +100 -125
  321. package/src/hooks/__tests__/useFileDisplay.test.ts +540 -0
  322. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1 -4
  323. package/src/hooks/__tests__/useFileUrl.unit.test.ts +27 -247
  324. package/src/hooks/__tests__/useFileUrlCache.test.ts +246 -56
  325. package/src/hooks/__tests__/useFocusManagement.unit.test.ts +442 -68
  326. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +345 -560
  327. package/src/hooks/__tests__/useFormDialog.test.ts +51 -222
  328. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +1 -1
  329. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +1 -4
  330. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +0 -1
  331. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +1 -1
  332. package/src/hooks/__tests__/usePermissionCache.test.ts +506 -0
  333. package/src/hooks/__tests__/usePreventTabReload.test.ts +255 -36
  334. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +17 -8
  335. package/src/hooks/__tests__/usePublicEvent.test.ts +16 -24
  336. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +12 -4
  337. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +3 -6
  338. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +1 -2
  339. package/src/hooks/__tests__/useQueryCache.test.ts +313 -66
  340. package/src/hooks/__tests__/useSessionDraft.test.ts +496 -103
  341. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +2 -2
  342. package/src/hooks/__tests__/useStorage.unit.test.ts +72 -102
  343. package/src/hooks/__tests__/useToast.test.ts +413 -0
  344. package/src/hooks/__tests__/useToast.unit.test.tsx +1 -1
  345. package/src/hooks/__tests__/useZodForm.unit.test.tsx +175 -21
  346. package/src/hooks/index.ts +13 -1
  347. package/src/hooks/public/usePublicEvent.test.ts +304 -0
  348. package/src/hooks/public/usePublicEvent.ts +11 -11
  349. package/src/hooks/public/usePublicEventLogo.test.ts +655 -120
  350. package/src/hooks/public/usePublicEventLogo.ts +2 -2
  351. package/src/hooks/public/usePublicFileDisplay.test.ts +723 -0
  352. package/src/hooks/public/usePublicFileDisplay.ts +79 -49
  353. package/src/hooks/public/usePublicRouteParams.test.ts +595 -0
  354. package/src/hooks/public/usePublicRouteParams.ts +2 -2
  355. package/src/hooks/services/useAuthService.ts +1 -1
  356. package/src/hooks/services/useEventService.ts +1 -1
  357. package/src/hooks/useAccessibleApps.test.ts +400 -0
  358. package/src/hooks/useAccessibleApps.ts +264 -0
  359. package/src/hooks/useAddressAutocomplete.test.ts +165 -42
  360. package/src/hooks/useAddressAutocomplete.ts +41 -28
  361. package/src/hooks/useAppConfig.ts +13 -3
  362. package/src/hooks/useDataTablePerformance.ts +13 -12
  363. package/src/hooks/useDataTableState.ts +5 -5
  364. package/src/hooks/useEventTheme.test.ts +66 -17
  365. package/src/hooks/useEventTheme.ts +1 -1
  366. package/src/hooks/useEvents.ts +8 -1
  367. package/src/hooks/useFileDisplay.ts +66 -33
  368. package/src/hooks/useFileReference.test.ts +365 -87
  369. package/src/hooks/useFileReference.ts +2 -6
  370. package/src/hooks/useFileUrlCache.ts +4 -1
  371. package/src/hooks/useFormDialog.ts +2 -2
  372. package/src/hooks/useInactivityTracker.ts +3 -3
  373. package/src/hooks/useOrganisationPermissions.test.ts +1 -2
  374. package/src/hooks/useOrganisationPermissions.ts +1 -4
  375. package/src/hooks/useOrganisationSecurity.test.ts +1 -30
  376. package/src/hooks/useOrganisationSecurity.ts +3 -3
  377. package/src/hooks/useOrganisations.ts +1 -1
  378. package/src/hooks/usePerformanceMonitor.ts +1 -1
  379. package/src/hooks/usePermissionCache.ts +2 -6
  380. package/src/hooks/useQueryCache.ts +7 -7
  381. package/src/hooks/useSessionDraft.ts +14 -11
  382. package/src/hooks/useSessionRestoration.ts +1 -1
  383. package/src/hooks/useStorage.ts +75 -40
  384. package/src/hooks/useToast.ts +2 -2
  385. package/src/hooks/useZodForm.ts +3 -3
  386. package/src/icons/__tests__/index.test.ts +133 -0
  387. package/src/icons/index.ts +1 -1
  388. package/src/index.ts +43 -4
  389. package/src/providers/OrganisationProvider.test.tsx +40 -0
  390. package/src/providers/OrganisationProvider.tsx +5 -5
  391. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +7 -12
  392. package/src/providers/__tests__/AuthProvider.test.tsx +22 -91
  393. package/src/providers/__tests__/EventProvider.test.tsx +16 -80
  394. package/src/providers/__tests__/InactivityProvider.test.tsx +29 -173
  395. package/src/providers/__tests__/OrganisationProvider.test.tsx +4 -5
  396. package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +591 -0
  397. package/src/providers/__tests__/ProviderLifecycle.test.tsx +80 -196
  398. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +40 -133
  399. package/src/providers/__tests__/index.test.ts +138 -0
  400. package/src/providers/services/AuthServiceContext.ts +27 -0
  401. package/src/providers/services/AuthServiceProvider.tsx +81 -20
  402. package/src/providers/services/EventServiceContext.ts +25 -0
  403. package/src/providers/services/EventServiceProvider.tsx +11 -20
  404. package/src/providers/services/InactivityServiceContext.ts +25 -0
  405. package/src/providers/services/InactivityServiceProvider.tsx +7 -17
  406. package/src/providers/services/OrganisationServiceContext.ts +25 -0
  407. package/src/providers/services/OrganisationServiceProvider.tsx +7 -17
  408. package/src/providers/services/UnifiedAuthContext.ts +99 -0
  409. package/src/providers/services/UnifiedAuthProvider.test.tsx +212 -0
  410. package/src/providers/services/UnifiedAuthProvider.tsx +38 -143
  411. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +61 -95
  412. package/src/providers/services/__tests__/AuthServiceProvider.test.tsx +638 -0
  413. package/src/providers/services/__tests__/EventServiceProvider.test.tsx +839 -0
  414. package/src/providers/services/__tests__/InactivityServiceProvider.test.tsx +662 -0
  415. package/src/providers/services/__tests__/OrganisationServiceProvider.test.tsx +440 -0
  416. package/src/providers/services/__tests__/UnifiedAuthProvider.advanced.test.tsx +435 -0
  417. package/src/providers/services/__tests__/UnifiedAuthProvider.appId.test.tsx +408 -0
  418. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +55 -48
  419. package/src/providers/services/__tests__/contexts.test.tsx +281 -0
  420. package/src/providers/services/__tests__/useUnifiedAuth.test.tsx +251 -0
  421. package/src/providers/services/useUnifiedAuth.ts +29 -0
  422. package/src/rbac/README.md +5 -5
  423. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +9 -14
  424. package/src/rbac/__tests__/audit-batched.test.ts +550 -0
  425. package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +1 -14
  426. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +43 -12
  427. package/src/rbac/__tests__/cache-invalidation.test.ts +8 -14
  428. package/src/rbac/__tests__/engine.comprehensive.test.ts +2 -7
  429. package/src/rbac/__tests__/index.test.ts +107 -0
  430. package/src/rbac/__tests__/performance.test.ts +451 -0
  431. package/src/rbac/__tests__/rbac-core.test.tsx +2 -2
  432. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +0 -5
  433. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +1 -7
  434. package/src/rbac/__tests__/rbac-functions.test.ts +0 -1
  435. package/src/rbac/__tests__/rbac-integration.test.ts +0 -1
  436. package/src/rbac/__tests__/scenarios.user-role.test.tsx +21 -32
  437. package/src/rbac/adapters.test.tsx +654 -0
  438. package/src/rbac/adapters.tsx +24 -9
  439. package/src/rbac/api.test.ts +13 -217
  440. package/src/rbac/api.ts +85 -16
  441. package/src/rbac/audit-batched.ts +5 -4
  442. package/src/rbac/audit.test.ts +225 -28
  443. package/src/rbac/audit.ts +22 -17
  444. package/src/rbac/cache-invalidation.ts +18 -15
  445. package/src/rbac/cache.test.ts +123 -63
  446. package/src/rbac/cache.ts +3 -4
  447. package/src/rbac/components/AccessDenied.tsx +20 -18
  448. package/src/rbac/components/NavigationGuard.tsx +10 -8
  449. package/src/rbac/components/PagePermissionGuard.tsx +27 -25
  450. package/src/rbac/components/__tests__/AccessDenied.test.tsx +324 -0
  451. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +242 -71
  452. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +20 -37
  453. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +18 -17
  454. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +452 -129
  455. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -13
  456. package/src/rbac/config.test.ts +131 -48
  457. package/src/rbac/config.ts +11 -8
  458. package/src/rbac/docs/event-based-apps.md +26 -13
  459. package/src/rbac/engine.test.ts +496 -146
  460. package/src/rbac/engine.ts +53 -13
  461. package/src/rbac/errors.test.ts +99 -87
  462. package/src/rbac/eslint-rules.js +2 -2
  463. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +0 -5
  464. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +601 -1
  465. package/src/rbac/hooks/permissions/__tests__/useAccessLevel.test.ts +622 -0
  466. package/src/rbac/hooks/permissions/__tests__/useCan.test.ts +798 -0
  467. package/src/rbac/hooks/permissions/__tests__/useMultiplePermissions.test.ts +843 -0
  468. package/src/rbac/hooks/permissions/__tests__/usePermissions.test.ts +545 -0
  469. package/src/rbac/hooks/permissions/useAccessLevel.ts +7 -8
  470. package/src/rbac/hooks/permissions/useCan.ts +12 -10
  471. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +57 -8
  472. package/src/rbac/hooks/permissions/usePermissions.ts +15 -14
  473. package/src/rbac/hooks/useCan.test.ts +319 -3
  474. package/src/rbac/hooks/usePermissions.test.ts +426 -0
  475. package/src/rbac/hooks/usePermissions.ts +5 -7
  476. package/src/rbac/hooks/useRBAC.test.ts +1669 -2
  477. package/src/rbac/hooks/useRBAC.ts +7 -11
  478. package/src/rbac/hooks/useResolvedScope.test.ts +442 -5
  479. package/src/rbac/hooks/useResolvedScope.ts +4 -1
  480. package/src/rbac/hooks/useResourcePermissions.test.ts +538 -1
  481. package/src/rbac/hooks/useResourcePermissions.ts +9 -7
  482. package/src/rbac/hooks/useRoleManagement.test.ts +659 -1
  483. package/src/rbac/hooks/useRoleManagement.ts +16 -12
  484. package/src/rbac/hooks/useSecureSupabase.ts +11 -12
  485. package/src/rbac/index.ts +32 -32
  486. package/src/rbac/permissions.test.ts +149 -68
  487. package/src/rbac/permissions.ts +0 -3
  488. package/src/rbac/request-deduplication.test.ts +347 -0
  489. package/src/rbac/secureClient.test.ts +112 -159
  490. package/src/rbac/secureClient.ts +46 -26
  491. package/src/rbac/security.test.ts +125 -44
  492. package/src/rbac/security.ts +7 -6
  493. package/src/rbac/types.test.ts +236 -0
  494. package/src/rbac/types.ts +7 -5
  495. package/src/rbac/utils/__tests__/clientSecurity.test.ts +192 -0
  496. package/src/rbac/utils/__tests__/contextValidator.test.ts +1 -3
  497. package/src/rbac/utils/__tests__/deep-equal.test.ts +23 -0
  498. package/src/rbac/utils/__tests__/eventContext.test.ts +10 -57
  499. package/src/rbac/utils/clientSecurity.ts +6 -4
  500. package/src/rbac/utils/contextValidator.ts +1 -2
  501. package/src/rbac/utils/eventContext.ts +2 -2
  502. package/src/services/AuthService.ts +13 -11
  503. package/src/services/EventService.ts +4 -5
  504. package/src/services/OrganisationService.ts +13 -30
  505. package/src/services/__tests__/AuthService.edge-cases.test.ts +746 -0
  506. package/src/services/__tests__/AuthService.restoreSession.test.ts +23 -3
  507. package/src/services/__tests__/AuthService.test.ts +4 -8
  508. package/src/services/__tests__/BaseService.edge-cases.test.ts +506 -0
  509. package/src/services/__tests__/BaseService.test.ts +49 -0
  510. package/src/services/__tests__/EventService.edge-cases.test.ts +633 -0
  511. package/src/services/__tests__/EventService.eventColours.test.ts +0 -12
  512. package/src/services/__tests__/EventService.test.ts +0 -7
  513. package/src/services/__tests__/InactivityService.edge-cases.test.ts +492 -0
  514. package/src/services/__tests__/InactivityService.lifecycle.test.ts +0 -5
  515. package/src/services/__tests__/OrganisationService.edge-cases.test.ts +633 -0
  516. package/src/services/base/BaseService.test.ts +214 -0
  517. package/src/services/interfaces/IOrganisationService.ts +0 -1
  518. package/src/services/interfaces/__tests__/IAuthService.test.ts +190 -0
  519. package/src/services/interfaces/__tests__/IEventService.test.ts +176 -0
  520. package/src/services/interfaces/__tests__/IInactivityService.test.ts +183 -0
  521. package/src/services/interfaces/__tests__/IOrganisationService.test.ts +207 -0
  522. package/src/styles/core.css +1 -0
  523. package/src/theming/__tests__/runtime.test.ts +29 -94
  524. package/src/theming/parseEventColours.ts +18 -9
  525. package/src/theming/runtime.ts +1 -5
  526. package/src/types/__tests__/core.test.ts +397 -0
  527. package/src/types/__tests__/database-generated.test.ts +78 -0
  528. package/src/types/__tests__/file-reference.test.ts +270 -366
  529. package/src/types/__tests__/guards.test.ts +26 -26
  530. package/src/types/__tests__/index.test.ts +265 -0
  531. package/src/types/__tests__/type-validation.test.ts +3 -3
  532. package/src/types/__tests__/validation.test.ts +0 -2
  533. package/src/types/auth.ts +0 -1
  534. package/src/types/database.generated.ts +9 -9
  535. package/src/types/event.ts +1 -1
  536. package/src/types/rpc-responses.ts +33 -0
  537. package/src/types/supabase.ts +1 -2
  538. package/src/types/vitest-globals.d.ts +1 -1
  539. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +64 -77
  540. package/src/utils/__tests__/dynamicUtils.unit.test.ts +13 -0
  541. package/src/utils/__tests__/formatDate.unit.test.ts +1 -1
  542. package/src/utils/__tests__/lazyLoad.unit.test.tsx +0 -1
  543. package/src/utils/__tests__/logger.unit.test.ts +1 -1
  544. package/src/utils/__tests__/performanceBenchmark.test.ts +1 -2
  545. package/src/utils/__tests__/performanceBudgets.unit.test.ts +48 -13
  546. package/src/utils/__tests__/request-deduplication.test.ts +349 -0
  547. package/src/utils/__tests__/secureDataAccess.unit.test.ts +0 -1
  548. package/src/utils/__tests__/timezone.test.ts +1 -1
  549. package/src/utils/__tests__/validation.unit.test.ts +1 -2
  550. package/src/utils/__tests__/validationUtils.unit.test.ts +1 -1
  551. package/src/utils/app/appConfig.test.ts +235 -0
  552. package/src/utils/app/appIdResolver.test.ts +188 -20
  553. package/src/utils/app/appNameResolver.test.ts +18 -10
  554. package/src/utils/app/appNameResolver.ts +11 -9
  555. package/src/utils/app/appPortMap.test.ts +125 -0
  556. package/src/utils/app/appPortMap.ts +51 -0
  557. package/src/utils/app/buildAppUrl.test.ts +273 -0
  558. package/src/utils/app/buildAppUrl.ts +114 -0
  559. package/src/utils/audit/audit.test.ts +354 -39
  560. package/src/utils/context/organisationContext.test.ts +10 -4
  561. package/src/utils/context/organisationContext.ts +5 -5
  562. package/src/utils/context/sessionTracking.test.ts +354 -0
  563. package/src/utils/core/__tests__/cn.test.ts +66 -0
  564. package/src/utils/core/__tests__/debugLogger.test.ts +113 -0
  565. package/src/utils/core/__tests__/logger.test.ts +217 -0
  566. package/src/utils/core/debugLogger.ts +15 -8
  567. package/src/utils/core/logger.ts +20 -16
  568. package/src/utils/device/deviceFingerprint.test.ts +8 -5
  569. package/src/utils/device/deviceFingerprint.ts +3 -3
  570. package/src/utils/dynamic/__tests__/dynamicUtils.test.ts +185 -0
  571. package/src/utils/dynamic/__tests__/lazyLoad.test.tsx +156 -0
  572. package/src/utils/dynamic/createLazyComponent.tsx +38 -0
  573. package/src/utils/dynamic/dynamicUtils.ts +6 -6
  574. package/src/utils/dynamic/lazyLoad.tsx +8 -36
  575. package/src/utils/dynamic/papaparseLoader.ts +7 -0
  576. package/src/utils/file-reference/__tests__/file-reference.test.ts +583 -145
  577. package/src/utils/file-reference/index.ts +0 -1
  578. package/src/utils/formatting/formatDate.test.ts +22 -148
  579. package/src/utils/formatting/formatDateTime.test.ts +41 -119
  580. package/src/utils/formatting/formatDateTimeTimezone.test.ts +40 -84
  581. package/src/utils/formatting/formatNumber.test.ts +259 -0
  582. package/src/utils/formatting/formatTime.test.ts +36 -128
  583. package/src/utils/formatting/formatting.ts +1 -1
  584. package/src/utils/google-places/googlePlacesUtils.test.ts +72 -3
  585. package/src/utils/google-places/googlePlacesUtils.ts +15 -2
  586. package/src/utils/google-places/loadGoogleMapsScript.test.ts +58 -1
  587. package/src/utils/google-places/loadGoogleMapsScript.ts +2 -1
  588. package/src/utils/index.ts +52 -11
  589. package/src/utils/location/location.test.ts +18 -115
  590. package/src/utils/performance/__tests__/bundleAnalysis.test.ts +148 -0
  591. package/src/utils/performance/__tests__/performanceBenchmark.test.ts +251 -0
  592. package/src/utils/performance/__tests__/performanceBudgets.test.ts +241 -0
  593. package/src/utils/performance/bundleAnalysis.ts +16 -22
  594. package/src/utils/performance/performanceBenchmark.ts +12 -4
  595. package/src/utils/performance/performanceBudgets.ts +9 -6
  596. package/src/utils/permissions/__tests__/permissionTypes.test.ts +149 -0
  597. package/src/utils/permissions/permissionUtils.test.ts +20 -42
  598. package/src/utils/persistence/__tests__/keyDerivation.test.ts +180 -9
  599. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +164 -16
  600. package/src/utils/persistence/sensitiveFieldDetection.ts +2 -2
  601. package/src/utils/request-deduplication.ts +6 -4
  602. package/src/utils/security/auth-utils.ts +7 -7
  603. package/src/utils/security/secureDataAccess.test.ts +22 -191
  604. package/src/utils/security/secureErrors.test.ts +163 -0
  605. package/src/utils/security/secureStorage.test.ts +156 -0
  606. package/src/utils/security/secureStorage.ts +1 -1
  607. package/src/utils/security/security.test.ts +204 -0
  608. package/src/utils/security/securityMonitor.test.ts +90 -0
  609. package/src/utils/security/securityMonitor.ts +1 -1
  610. package/src/utils/storage/__tests__/config.unit.test.ts +239 -0
  611. package/src/utils/storage/__tests__/index.unit.test.ts +64 -12
  612. package/src/utils/storage/helpers.test.ts +757 -430
  613. package/src/utils/storage/helpers.ts +1 -2
  614. package/src/utils/storage/{index.ts → storageUtils.ts} +1 -36
  615. package/src/utils/storage/types.ts +2 -2
  616. package/src/utils/supabase/createBaseClient.test.ts +201 -0
  617. package/src/utils/supabase/createBaseClient.ts +27 -8
  618. package/src/utils/timezone/timezone.test.ts +25 -43
  619. package/src/utils/validation/__tests__/common.test.ts +115 -0
  620. package/src/utils/validation/__tests__/csrf.test.ts +65 -0
  621. package/src/utils/validation/__tests__/htmlSanitization.unit.test.ts +27 -7
  622. package/src/utils/validation/__tests__/passwordSchema.test.ts +164 -0
  623. package/src/utils/validation/__tests__/schema.test.ts +127 -0
  624. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +76 -3
  625. package/src/utils/validation/__tests__/user.test.ts +173 -0
  626. package/src/utils/validation/__tests__/validation.test.ts +197 -0
  627. package/src/utils/validation/__tests__/validationUtils.test.ts +265 -43
  628. package/src/utils/validation/htmlSanitization.ts +27 -31
  629. package/src/utils/validation/schema.ts +6 -3
  630. package/src/utils/validation/sqlInjectionProtection.ts +2 -2
  631. package/src/vite-env.d.ts +6 -0
  632. package/dist/DataTable-DRUIgtUH.d.ts +0 -166
  633. package/dist/UnifiedAuthProvider-7SNDOWYD.js +0 -7
  634. package/dist/audit-MYQXYZFU.js +0 -3
  635. package/dist/chunk-7ILTDCL2.js +0 -80
  636. package/dist/chunk-EF2UGZWY.js +0 -611
  637. package/dist/chunk-FEJLJNWA.js +0 -181
  638. package/dist/chunk-GS5672WG.js +0 -2003
  639. package/dist/chunk-S6ZQKDY6.js +0 -62
  640. package/dist/chunk-Z2FNRKF3.js +0 -994
  641. package/dist/useToast-AyaT-x7p.d.ts +0 -68
  642. package/src/components/DataTable/components/index.ts +0 -16
  643. package/src/components/DataTable/core/index.ts +0 -1
  644. package/src/components/DataTable/hooks/index.ts +0 -13
  645. package/src/components/DataTable/utils/index.ts +0 -9
  646. package/src/components/PublicLayout/index.ts +0 -32
  647. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  648. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  649. package/src/hooks/public/index.ts +0 -36
  650. package/src/hooks/usePermissionCache.test.ts +0 -536
  651. package/src/rbac/__tests__/isSuperAdmin.real.test.ts +0 -82
  652. package/src/rbac/audit-enhanced.ts +0 -384
  653. package/src/rbac/compliance/database-validator.ts +0 -165
  654. package/src/rbac/compliance/index.ts +0 -48
  655. package/src/rbac/compliance/pattern-detector.ts +0 -553
  656. package/src/rbac/compliance/quick-fix-suggestions.ts +0 -209
  657. package/src/rbac/compliance/runtime-compliance.ts +0 -99
  658. package/src/rbac/compliance/setup-validator.ts +0 -131
  659. package/src/rbac/components/index.ts +0 -26
  660. package/src/rbac/hooks/index.ts +0 -34
  661. package/src/rbac/hooks/permissions/index.ts +0 -4
  662. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  663. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -490
  664. package/src/utils/app/appNameResolver.simple.test.ts +0 -212
  665. package/src/utils/google-places/index.ts +0 -26
  666. package/src/utils/location/index.ts +0 -16
  667. package/src/utils/storage/__tests__/helpers.unit.test.ts +0 -332
  668. package/src/utils/timezone/index.ts +0 -17
  669. package/src/utils/validation/index.ts +0 -73
@@ -200,7 +200,7 @@ describe('NavigationMenu Component', () => {
200
200
 
201
201
  // Basic rendering tests
202
202
  describe('Rendering', () => {
203
- it('renders dropdown mode by default', () => {
203
+ it('renders dropdown mode by default with button text', () => {
204
204
  renderWithProviders(
205
205
  <NavigationMenu
206
206
  items={basicNavItems}
@@ -213,46 +213,22 @@ describe('NavigationMenu Component', () => {
213
213
  expect(screen.getByText('Main Menu')).toBeInTheDocument();
214
214
  });
215
215
 
216
- it('renders hierarchical mode when specified', () => {
216
+ it('renders hierarchical mode with proper ARIA structure', () => {
217
217
  renderWithProviders(
218
218
  <NavigationMenu
219
219
  items={hierarchicalNavItems}
220
220
  mode="hierarchical"
221
221
  onNavigate={mockNavigate}
222
+ navigationLabel="Custom Navigation"
222
223
  />
223
224
  );
224
225
 
225
226
  expect(screen.getByRole('navigation')).toBeInTheDocument();
226
227
  expect(screen.getByRole('menubar')).toBeInTheDocument();
227
- });
228
-
229
- it('renders with custom className', () => {
230
- renderWithProviders(
231
- <NavigationMenu
232
- items={basicNavItems}
233
- onNavigate={mockNavigate}
234
- className="custom-nav-class"
235
- />
236
- );
237
-
238
- const navElement = screen.getByRole('combobox').closest('.custom-nav-class');
239
- expect(navElement).toBeInTheDocument();
240
- });
241
-
242
- it('renders with custom navigation label', () => {
243
- renderWithProviders(
244
- <NavigationMenu
245
- items={basicNavItems}
246
- mode="hierarchical"
247
- onNavigate={mockNavigate}
248
- navigationLabel="Custom Navigation"
249
- />
250
- );
251
-
252
228
  expect(screen.getByLabelText('Custom Navigation')).toBeInTheDocument();
253
229
  });
254
230
 
255
- it('omits items marked as hidden via metadata', async () => {
231
+ it('applies custom className and omits hidden items', async () => {
256
232
  const user = userEvent.setup();
257
233
  const hiddenItems: NavigationItem[] = [
258
234
  { id: 'home', label: 'Home', href: '/' },
@@ -263,10 +239,14 @@ describe('NavigationMenu Component', () => {
263
239
  <NavigationMenu
264
240
  items={hiddenItems}
265
241
  onNavigate={mockNavigate}
242
+ className="custom-nav-class"
266
243
  buttonText="Menu"
267
244
  />
268
245
  );
269
246
 
247
+ const navElement = screen.getByRole('combobox').closest('.custom-nav-class');
248
+ expect(navElement).toBeInTheDocument();
249
+
270
250
  await user.click(screen.getByRole('combobox'));
271
251
  await waitFor(() => {
272
252
  expect(screen.getByText('Home')).toBeInTheDocument();
@@ -278,20 +258,7 @@ describe('NavigationMenu Component', () => {
278
258
 
279
259
  // Dropdown mode tests
280
260
  describe('Dropdown Mode', () => {
281
- it('renders dropdown trigger with button text', () => {
282
- renderWithProviders(
283
- <NavigationMenu
284
- items={basicNavItems}
285
- onNavigate={mockNavigate}
286
- buttonText="Custom Menu"
287
- />
288
- );
289
-
290
- expect(screen.getByRole('combobox')).toBeInTheDocument();
291
- expect(screen.getByText('Custom Menu')).toBeInTheDocument();
292
- });
293
-
294
- it('renders all navigation items in dropdown', async () => {
261
+ it('renders all navigation items and handles selection', async () => {
295
262
  const user = userEvent.setup();
296
263
  renderWithProviders(
297
264
  <NavigationMenu
@@ -309,24 +276,6 @@ describe('NavigationMenu Component', () => {
309
276
  expect(screen.getByText('Dashboard')).toBeInTheDocument();
310
277
  expect(screen.getByText('Settings')).toBeInTheDocument();
311
278
  }, { interval: 10 });
312
- });
313
-
314
- it('handles navigation item selection', async () => {
315
- const user = userEvent.setup();
316
- renderWithProviders(
317
- <NavigationMenu
318
- items={basicNavItems}
319
- onNavigate={mockNavigate}
320
- buttonText="Menu"
321
- />
322
- );
323
-
324
- const trigger = screen.getByRole('combobox');
325
- await user.click(trigger);
326
-
327
- await waitFor(() => {
328
- expect(screen.getByText('Dashboard')).toBeInTheDocument();
329
- }, { interval: 10 });
330
279
 
331
280
  const dashboardItem = screen.getByText('Dashboard');
332
281
  await user.click(dashboardItem);
@@ -354,26 +303,6 @@ describe('NavigationMenu Component', () => {
354
303
  expect(trigger).toBeDisabled();
355
304
  });
356
305
 
357
- it('highlights active item based on currentPath', async () => {
358
- const user = userEvent.setup();
359
- renderWithProviders(
360
- <NavigationMenu
361
- items={basicNavItems}
362
- onNavigate={mockNavigate}
363
- currentPath="/dashboard"
364
- buttonText="Menu"
365
- />
366
- );
367
-
368
- const trigger = screen.getByRole('combobox');
369
- await user.click(trigger);
370
-
371
- await waitFor(() => {
372
- const dashboardItem = screen.getByText('Dashboard');
373
- expect(dashboardItem).toBeInTheDocument();
374
- }, { interval: 10 });
375
- });
376
-
377
306
  it('trusts pre-filtered items while still hiding meta-hidden entries', async () => {
378
307
  const user = userEvent.setup();
379
308
 
@@ -728,6 +657,34 @@ describe('NavigationMenu Component', () => {
728
657
  );
729
658
  });
730
659
 
660
+ it('allows navigation when auth context is unavailable', async () => {
661
+ const user = userEvent.setup();
662
+ mockUseUnifiedAuthFn.mockImplementationOnce(() => null as any);
663
+
664
+ renderWithProviders(
665
+ <NavigationMenu
666
+ items={basicNavItems}
667
+ onNavigate={mockNavigate}
668
+ buttonText="Menu"
669
+ itemsPreFiltered={true}
670
+ />
671
+ );
672
+
673
+ const trigger = screen.getByRole('combobox');
674
+ await user.click(trigger);
675
+
676
+ const homeItem = await screen.findByRole('option', { name: 'Home' });
677
+ await user.click(homeItem);
678
+
679
+ expect(mockNavigate).toHaveBeenCalledWith(
680
+ expect.objectContaining({
681
+ id: 'home',
682
+ label: 'Home',
683
+ href: '/',
684
+ })
685
+ );
686
+ });
687
+
731
688
  it('handles navigation without onNavigate callback', async () => {
732
689
  const user = userEvent.setup();
733
690
  // Mock window.location.href
@@ -1409,8 +1366,8 @@ describe('NavigationMenu Component', () => {
1409
1366
  // Since hooks are now unconditional, they will throw if providers are missing
1410
1367
  // The component should be wrapped in an error boundary in real apps
1411
1368
  // For this test, we expect it to throw, which should be caught by error boundaries
1412
- mockUseUnifiedAuthFn.mockImplementation(() => { throw new Error('no auth'); });
1413
- mockUseRBAC.mockImplementation(() => { throw new Error('no rbac'); });
1369
+ mockUseUnifiedAuthFn.mockImplementationOnce(() => { throw new Error('no auth'); });
1370
+ mockUseRBAC.mockImplementationOnce(() => { throw new Error('no rbac'); });
1414
1371
 
1415
1372
  mockUsePermissions.mockReturnValue({
1416
1373
  permissions: { '*': true } as any,
@@ -1534,4 +1491,1364 @@ describe('NavigationMenu Component', () => {
1534
1491
  );
1535
1492
  });
1536
1493
  });
1494
+
1495
+ describe('Deeply Nested Hierarchical Items', () => {
1496
+ it('renders and expands 3+ level nested items', async () => {
1497
+ const user = userEvent.setup();
1498
+ const deeplyNestedItems: NavigationItem[] = [
1499
+ {
1500
+ id: 'level1',
1501
+ label: 'Level 1',
1502
+ children: [
1503
+ {
1504
+ id: 'level2',
1505
+ label: 'Level 2',
1506
+ children: [
1507
+ {
1508
+ id: 'level3',
1509
+ label: 'Level 3',
1510
+ children: [
1511
+ { id: 'level4', label: 'Level 4', href: '/level4' },
1512
+ ],
1513
+ },
1514
+ ],
1515
+ },
1516
+ ],
1517
+ },
1518
+ ];
1519
+
1520
+ renderWithProviders(
1521
+ <NavigationMenu
1522
+ items={deeplyNestedItems}
1523
+ mode="hierarchical"
1524
+ onNavigate={mockNavigate}
1525
+ />
1526
+ );
1527
+
1528
+ // Expand level 1
1529
+ const level1 = screen.getByRole('button', { name: /Level 1/i });
1530
+ await user.click(level1);
1531
+
1532
+ await waitFor(() => {
1533
+ expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
1534
+ });
1535
+
1536
+ // Expand level 2
1537
+ const level2 = screen.getByRole('button', { name: /Level 2/i });
1538
+ await user.click(level2);
1539
+
1540
+ await waitFor(() => {
1541
+ expect(screen.getByRole('button', { name: /Level 3/i })).toBeInTheDocument();
1542
+ });
1543
+
1544
+ // Expand level 3
1545
+ const level3 = screen.getByRole('button', { name: /Level 3/i });
1546
+ await user.click(level3);
1547
+
1548
+ await waitFor(() => {
1549
+ expect(screen.getByText('Level 4')).toBeInTheDocument();
1550
+ });
1551
+ });
1552
+
1553
+ it('handles keyboard navigation in deeply nested structures', async () => {
1554
+ const user = userEvent.setup();
1555
+ const deeplyNestedItems: NavigationItem[] = [
1556
+ {
1557
+ id: 'level1',
1558
+ label: 'Level 1',
1559
+ children: [
1560
+ {
1561
+ id: 'level2',
1562
+ label: 'Level 2',
1563
+ children: [
1564
+ { id: 'level3', label: 'Level 3', href: '/level3' },
1565
+ ],
1566
+ },
1567
+ ],
1568
+ },
1569
+ ];
1570
+
1571
+ renderWithProviders(
1572
+ <NavigationMenu
1573
+ items={deeplyNestedItems}
1574
+ mode="hierarchical"
1575
+ onNavigate={mockNavigate}
1576
+ />
1577
+ );
1578
+
1579
+ // Navigate with keyboard
1580
+ const level1 = screen.getByRole('button', { name: /Level 1/i });
1581
+ level1.focus();
1582
+ await user.keyboard('{Enter}');
1583
+
1584
+ await waitFor(() => {
1585
+ expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
1586
+ });
1587
+
1588
+ const level2 = screen.getByRole('button', { name: /Level 2/i });
1589
+ level2.focus();
1590
+ await user.keyboard('{Enter}');
1591
+
1592
+ await waitFor(() => {
1593
+ expect(screen.getByText('Level 3')).toBeInTheDocument();
1594
+ });
1595
+ });
1596
+
1597
+ it('highlights active items in deeply nested structures', async () => {
1598
+ const user = userEvent.setup();
1599
+ const deeplyNestedItems: NavigationItem[] = [
1600
+ {
1601
+ id: 'level1',
1602
+ label: 'Level 1',
1603
+ children: [
1604
+ {
1605
+ id: 'level2',
1606
+ label: 'Level 2',
1607
+ children: [
1608
+ { id: 'level3', label: 'Level 3', href: '/level3' },
1609
+ ],
1610
+ },
1611
+ ],
1612
+ },
1613
+ ];
1614
+
1615
+ renderWithProviders(
1616
+ <NavigationMenu
1617
+ items={deeplyNestedItems}
1618
+ mode="hierarchical"
1619
+ onNavigate={mockNavigate}
1620
+ currentPath="/level3"
1621
+ />
1622
+ );
1623
+
1624
+ // Expand all levels
1625
+ const level1 = screen.getByRole('button', { name: /Level 1/i });
1626
+ await user.click(level1);
1627
+
1628
+ await waitFor(() => {
1629
+ expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
1630
+ });
1631
+
1632
+ const level2 = screen.getByRole('button', { name: /Level 2/i });
1633
+ await user.click(level2);
1634
+
1635
+ await waitFor(() => {
1636
+ const level3 = screen.getByText('Level 3');
1637
+ expect(level3).toBeInTheDocument();
1638
+ // Active item should be highlighted
1639
+ expect(level3.closest('a') || level3.closest('button')).toHaveAttribute('aria-current', 'page');
1640
+ });
1641
+ });
1642
+ });
1643
+
1644
+ describe('Items with Both href and children', () => {
1645
+ it('renders parent item with href and expandable children', async () => {
1646
+ const user = userEvent.setup();
1647
+ const itemsWithHrefAndChildren: NavigationItem[] = [
1648
+ {
1649
+ id: 'parent',
1650
+ label: 'Parent',
1651
+ href: '/parent',
1652
+ children: [
1653
+ { id: 'child1', label: 'Child 1', href: '/child1' },
1654
+ { id: 'child2', label: 'Child 2', href: '/child2' },
1655
+ ],
1656
+ },
1657
+ ];
1658
+
1659
+ renderWithProviders(
1660
+ <NavigationMenu
1661
+ items={itemsWithHrefAndChildren}
1662
+ mode="hierarchical"
1663
+ onNavigate={mockNavigate}
1664
+ />
1665
+ );
1666
+
1667
+ // In hierarchical mode, items with children are rendered as buttons for expansion
1668
+ const parent = screen.getByRole('button', { name: /Parent/i });
1669
+ expect(parent).toBeInTheDocument();
1670
+
1671
+ // Parent should be expandable
1672
+ await user.click(parent);
1673
+
1674
+ await waitFor(() => {
1675
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
1676
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
1677
+ });
1678
+ });
1679
+
1680
+ it('handles navigation for parent href', async () => {
1681
+ const user = userEvent.setup();
1682
+ const itemsWithHrefAndChildren: NavigationItem[] = [
1683
+ {
1684
+ id: 'parent',
1685
+ label: 'Parent',
1686
+ href: '/parent',
1687
+ children: [
1688
+ { id: 'child1', label: 'Child 1', href: '/child1' },
1689
+ ],
1690
+ },
1691
+ ];
1692
+
1693
+ renderWithProviders(
1694
+ <NavigationMenu
1695
+ items={itemsWithHrefAndChildren}
1696
+ mode="hierarchical"
1697
+ onNavigate={mockNavigate}
1698
+ />
1699
+ );
1700
+
1701
+ // In hierarchical mode, items with children are buttons
1702
+ // Clicking the button should trigger navigation if href exists
1703
+ const parentButton = screen.getByRole('button', { name: /Parent/i });
1704
+ await user.click(parentButton);
1705
+
1706
+ // The component should handle navigation for items with href
1707
+ // Note: The actual behavior depends on component implementation
1708
+ // If the component supports clicking to navigate when href exists,
1709
+ // mockNavigate should be called. Otherwise, it may only expand.
1710
+ // For now, we verify the button exists and is clickable
1711
+ expect(parentButton).toBeInTheDocument();
1712
+ });
1713
+
1714
+ it('handles expansion for children', async () => {
1715
+ const user = userEvent.setup();
1716
+ const itemsWithHrefAndChildren: NavigationItem[] = [
1717
+ {
1718
+ id: 'parent',
1719
+ label: 'Parent',
1720
+ href: '/parent',
1721
+ children: [
1722
+ { id: 'child1', label: 'Child 1', href: '/child1' },
1723
+ ],
1724
+ },
1725
+ ];
1726
+
1727
+ renderWithProviders(
1728
+ <NavigationMenu
1729
+ items={itemsWithHrefAndChildren}
1730
+ mode="hierarchical"
1731
+ onNavigate={mockNavigate}
1732
+ />
1733
+ );
1734
+
1735
+ // Click expand button (not link)
1736
+ const expandButton = screen.getByRole('button', { name: /Parent/i });
1737
+ await user.click(expandButton);
1738
+
1739
+ await waitFor(() => {
1740
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
1741
+ });
1742
+
1743
+ // Click child
1744
+ const child = screen.getByText('Child 1');
1745
+ await user.click(child);
1746
+
1747
+ expect(mockNavigate).toHaveBeenCalledWith(
1748
+ expect.objectContaining({
1749
+ id: 'child1',
1750
+ href: '/child1',
1751
+ })
1752
+ );
1753
+ });
1754
+ });
1755
+
1756
+ describe('Page ID Handling', () => {
1757
+ it('uses explicit pageId when provided', async () => {
1758
+ const user = userEvent.setup();
1759
+ const itemsWithPageId: NavigationItem[] = [
1760
+ { id: 'custom', label: 'Custom', href: '/custom-path', pageId: 'custom-page' },
1761
+ ];
1762
+
1763
+ renderWithProviders(
1764
+ <NavigationMenu
1765
+ items={itemsWithPageId}
1766
+ onNavigate={mockNavigate}
1767
+ buttonText="Menu"
1768
+ />
1769
+ );
1770
+
1771
+ await user.click(screen.getByRole('combobox'));
1772
+
1773
+ await waitFor(() => {
1774
+ expect(screen.getByRole('option', { name: 'Custom' })).toBeInTheDocument();
1775
+ });
1776
+
1777
+ const customItem = screen.getByRole('option', { name: 'Custom' });
1778
+ await user.click(customItem);
1779
+
1780
+ // Navigation should use the item data which includes pageId
1781
+ expect(mockNavigate).toHaveBeenCalledWith(
1782
+ expect.objectContaining({
1783
+ id: 'custom',
1784
+ href: '/custom-path',
1785
+ pageId: 'custom-page',
1786
+ })
1787
+ );
1788
+ });
1789
+
1790
+ it('derives pageId from href when pageId not provided', async () => {
1791
+ const user = userEvent.setup();
1792
+ const itemsWithoutPageId: NavigationItem[] = [
1793
+ { id: 'derived', label: 'Derived', href: '/dashboard' },
1794
+ ];
1795
+
1796
+ renderWithProviders(
1797
+ <NavigationMenu
1798
+ items={itemsWithoutPageId}
1799
+ onNavigate={mockNavigate}
1800
+ buttonText="Menu"
1801
+ />
1802
+ );
1803
+
1804
+ await user.click(screen.getByRole('combobox'));
1805
+
1806
+ await waitFor(() => {
1807
+ expect(screen.getByRole('option', { name: 'Derived' })).toBeInTheDocument();
1808
+ });
1809
+
1810
+ // The hook derives pageId from href (dashboard -> read:page.dashboard)
1811
+ // Navigation should work correctly
1812
+ const derivedItem = screen.getByRole('option', { name: 'Derived' });
1813
+ await user.click(derivedItem);
1814
+
1815
+ expect(mockNavigate).toHaveBeenCalledWith(
1816
+ expect.objectContaining({
1817
+ id: 'derived',
1818
+ href: '/dashboard',
1819
+ })
1820
+ );
1821
+ });
1822
+
1823
+ it('handles complex hrefs with query params and hash', async () => {
1824
+ const user = userEvent.setup();
1825
+ const itemsWithComplexHref: NavigationItem[] = [
1826
+ { id: 'complex', label: 'Complex', href: '/page?param=value#hash' },
1827
+ ];
1828
+
1829
+ renderWithProviders(
1830
+ <NavigationMenu
1831
+ items={itemsWithComplexHref}
1832
+ onNavigate={mockNavigate}
1833
+ buttonText="Menu"
1834
+ />
1835
+ );
1836
+
1837
+ await user.click(screen.getByRole('combobox'));
1838
+
1839
+ await waitFor(() => {
1840
+ expect(screen.getByRole('option', { name: 'Complex' })).toBeInTheDocument();
1841
+ });
1842
+
1843
+ const complexItem = screen.getByRole('option', { name: 'Complex' });
1844
+ await user.click(complexItem);
1845
+
1846
+ // Navigation should preserve the full href
1847
+ expect(mockNavigate).toHaveBeenCalledWith(
1848
+ expect.objectContaining({
1849
+ id: 'complex',
1850
+ href: '/page?param=value#hash',
1851
+ })
1852
+ );
1853
+ });
1854
+ });
1855
+
1856
+ describe('Multiple Expanded Items', () => {
1857
+ it('expands multiple hierarchical items simultaneously', async () => {
1858
+ const user = userEvent.setup();
1859
+ const multipleExpandableItems: NavigationItem[] = [
1860
+ {
1861
+ id: 'section1',
1862
+ label: 'Section 1',
1863
+ children: [
1864
+ { id: 'item1', label: 'Item 1', href: '/item1' },
1865
+ ],
1866
+ },
1867
+ {
1868
+ id: 'section2',
1869
+ label: 'Section 2',
1870
+ children: [
1871
+ { id: 'item2', label: 'Item 2', href: '/item2' },
1872
+ ],
1873
+ },
1874
+ ];
1875
+
1876
+ renderWithProviders(
1877
+ <NavigationMenu
1878
+ items={multipleExpandableItems}
1879
+ mode="hierarchical"
1880
+ onNavigate={mockNavigate}
1881
+ />
1882
+ );
1883
+
1884
+ // Expand first section
1885
+ const section1 = screen.getByRole('button', { name: /Section 1/i });
1886
+ await user.click(section1);
1887
+
1888
+ await waitFor(() => {
1889
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
1890
+ });
1891
+
1892
+ // Expand second section (first should remain expanded)
1893
+ const section2 = screen.getByRole('button', { name: /Section 2/i });
1894
+ await user.click(section2);
1895
+
1896
+ await waitFor(() => {
1897
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
1898
+ // First section should still be expanded
1899
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
1900
+ });
1901
+ });
1902
+
1903
+ it('collapses one while keeping others expanded', async () => {
1904
+ const user = userEvent.setup();
1905
+ const multipleExpandableItems: NavigationItem[] = [
1906
+ {
1907
+ id: 'section1',
1908
+ label: 'Section 1',
1909
+ children: [
1910
+ { id: 'item1', label: 'Item 1', href: '/item1' },
1911
+ ],
1912
+ },
1913
+ {
1914
+ id: 'section2',
1915
+ label: 'Section 2',
1916
+ children: [
1917
+ { id: 'item2', label: 'Item 2', href: '/item2' },
1918
+ ],
1919
+ },
1920
+ ];
1921
+
1922
+ renderWithProviders(
1923
+ <NavigationMenu
1924
+ items={multipleExpandableItems}
1925
+ mode="hierarchical"
1926
+ onNavigate={mockNavigate}
1927
+ />
1928
+ );
1929
+
1930
+ // Expand both sections
1931
+ const section1 = screen.getByRole('button', { name: /Section 1/i });
1932
+ await user.click(section1);
1933
+
1934
+ const section2 = screen.getByRole('button', { name: /Section 2/i });
1935
+ await user.click(section2);
1936
+
1937
+ await waitFor(() => {
1938
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
1939
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
1940
+ });
1941
+
1942
+ // Collapse first section
1943
+ await user.click(section1);
1944
+
1945
+ await waitFor(() => {
1946
+ // First section should be collapsed
1947
+ expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
1948
+ // Second section should still be expanded
1949
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
1950
+ });
1951
+ });
1952
+
1953
+ it('handles keyboard navigation across multiple expanded sections', async () => {
1954
+ const user = userEvent.setup();
1955
+ const multipleExpandableItems: NavigationItem[] = [
1956
+ {
1957
+ id: 'section1',
1958
+ label: 'Section 1',
1959
+ children: [
1960
+ { id: 'item1', label: 'Item 1', href: '/item1' },
1961
+ ],
1962
+ },
1963
+ {
1964
+ id: 'section2',
1965
+ label: 'Section 2',
1966
+ children: [
1967
+ { id: 'item2', label: 'Item 2', href: '/item2' },
1968
+ ],
1969
+ },
1970
+ ];
1971
+
1972
+ renderWithProviders(
1973
+ <NavigationMenu
1974
+ items={multipleExpandableItems}
1975
+ mode="hierarchical"
1976
+ onNavigate={mockNavigate}
1977
+ />
1978
+ );
1979
+
1980
+ // Expand both sections with keyboard
1981
+ const section1 = screen.getByRole('button', { name: /Section 1/i });
1982
+ section1.focus();
1983
+ await user.keyboard('{Enter}');
1984
+
1985
+ const section2 = screen.getByRole('button', { name: /Section 2/i });
1986
+ section2.focus();
1987
+ await user.keyboard('{Enter}');
1988
+
1989
+ await waitFor(() => {
1990
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
1991
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
1992
+ });
1993
+
1994
+ // Navigate to item in first section
1995
+ const item1 = screen.getByText('Item 1');
1996
+ item1.focus();
1997
+ await user.keyboard('{Enter}');
1998
+
1999
+ expect(mockNavigate).toHaveBeenCalledWith(
2000
+ expect.objectContaining({
2001
+ id: 'item1',
2002
+ href: '/item1',
2003
+ })
2004
+ );
2005
+ });
2006
+ });
2007
+
2008
+ describe('Permission Edge Cases', () => {
2009
+ it('handles rapid permission state changes gracefully', async () => {
2010
+ const user = userEvent.setup();
2011
+ const { rerender } = renderWithProviders(
2012
+ <NavigationMenu
2013
+ items={basicNavItems}
2014
+ onNavigate={mockNavigate}
2015
+ buttonText="Menu"
2016
+ itemsPreFiltered={true}
2017
+ />
2018
+ );
2019
+
2020
+ // Start with permissions
2021
+ mockUsePermissions.mockReturnValue({
2022
+ permissions: { '*': true } as any,
2023
+ isLoading: false,
2024
+ error: null,
2025
+ hasPermission: vi.fn(() => true),
2026
+ hasAnyPermission: vi.fn(() => true),
2027
+ hasAllPermissions: vi.fn(() => true),
2028
+ refetch: vi.fn(),
2029
+ });
2030
+
2031
+ // Open menu
2032
+ await user.click(screen.getByRole('combobox'));
2033
+ await waitFor(() => {
2034
+ expect(screen.getByRole('option', { name: 'Home' })).toBeInTheDocument();
2035
+ }, { interval: 10 });
2036
+
2037
+ // Rapidly change permissions to loading
2038
+ mockUsePermissions.mockReturnValue({
2039
+ permissions: { '*': true } as any,
2040
+ isLoading: true,
2041
+ error: null,
2042
+ hasPermission: vi.fn(() => true),
2043
+ hasAnyPermission: vi.fn(() => true),
2044
+ hasAllPermissions: vi.fn(() => true),
2045
+ refetch: vi.fn(),
2046
+ });
2047
+
2048
+ rerender(
2049
+ <NavigationMenu
2050
+ items={basicNavItems}
2051
+ onNavigate={mockNavigate}
2052
+ buttonText="Menu"
2053
+ itemsPreFiltered={true}
2054
+ />
2055
+ );
2056
+
2057
+ // Change back to loaded
2058
+ mockUsePermissions.mockReturnValue({
2059
+ permissions: { '*': true } as any,
2060
+ isLoading: false,
2061
+ error: null,
2062
+ hasPermission: vi.fn(() => true),
2063
+ hasAnyPermission: vi.fn(() => true),
2064
+ hasAllPermissions: vi.fn(() => true),
2065
+ refetch: vi.fn(),
2066
+ });
2067
+
2068
+ rerender(
2069
+ <NavigationMenu
2070
+ items={basicNavItems}
2071
+ onNavigate={mockNavigate}
2072
+ buttonText="Menu"
2073
+ itemsPreFiltered={true}
2074
+ />
2075
+ );
2076
+
2077
+ // Component should handle rapid changes without crashing
2078
+ await waitFor(() => {
2079
+ const combobox = screen.getByRole('combobox');
2080
+ expect(combobox).toBeInTheDocument();
2081
+ });
2082
+ });
2083
+ });
2084
+
2085
+ describe('Current Path Matching', () => {
2086
+ it('matches exact current path in dropdown mode', async () => {
2087
+ const user = userEvent.setup();
2088
+ renderWithProviders(
2089
+ <NavigationMenu
2090
+ items={basicNavItems}
2091
+ onNavigate={mockNavigate}
2092
+ currentPath="/dashboard"
2093
+ buttonText="Menu"
2094
+ />
2095
+ );
2096
+
2097
+ await user.click(screen.getByRole('combobox'));
2098
+ await waitFor(() => {
2099
+ const dashboardItem = screen.getByText('Dashboard');
2100
+ expect(dashboardItem).toBeInTheDocument();
2101
+ }, { interval: 10 });
2102
+ });
2103
+
2104
+ it('matches current path in hierarchical mode with nested items', () => {
2105
+ const nestedItems: NavigationItem[] = [
2106
+ {
2107
+ id: 'parent',
2108
+ label: 'Parent',
2109
+ children: [
2110
+ { id: 'child1', label: 'Child 1', href: '/parent/child1' },
2111
+ { id: 'child2', label: 'Child 2', href: '/parent/child2' },
2112
+ ],
2113
+ },
2114
+ ];
2115
+
2116
+ renderWithProviders(
2117
+ <NavigationMenu
2118
+ items={nestedItems}
2119
+ mode="hierarchical"
2120
+ onNavigate={mockNavigate}
2121
+ currentPath="/parent/child1"
2122
+ />
2123
+ );
2124
+
2125
+ const parentButton = screen.getByRole('button', { name: /Parent/i });
2126
+ expect(parentButton).toHaveAttribute('aria-current', 'page');
2127
+ });
2128
+
2129
+ it('does not match partial path segments', () => {
2130
+ renderWithProviders(
2131
+ <NavigationMenu
2132
+ items={basicNavItems}
2133
+ mode="hierarchical"
2134
+ onNavigate={mockNavigate}
2135
+ currentPath="/dashboard-settings"
2136
+ />
2137
+ );
2138
+
2139
+ // Should not match /dashboard when currentPath is /dashboard-settings
2140
+ const dashboardLink = screen.queryByText('Dashboard');
2141
+ if (dashboardLink) {
2142
+ expect(dashboardLink.closest('a')).not.toHaveAttribute('aria-current', 'page');
2143
+ }
2144
+ });
2145
+
2146
+ it('handles root path (/) correctly', () => {
2147
+ renderWithProviders(
2148
+ <NavigationMenu
2149
+ items={basicNavItems}
2150
+ mode="hierarchical"
2151
+ onNavigate={mockNavigate}
2152
+ currentPath="/"
2153
+ />
2154
+ );
2155
+
2156
+ const homeLink = screen.getByText('Home');
2157
+ expect(homeLink.closest('a')).toHaveAttribute('aria-current', 'page');
2158
+ });
2159
+
2160
+ it('handles undefined currentPath gracefully', () => {
2161
+ renderWithProviders(
2162
+ <NavigationMenu
2163
+ items={basicNavItems}
2164
+ mode="hierarchical"
2165
+ onNavigate={mockNavigate}
2166
+ />
2167
+ );
2168
+
2169
+ // Should render without errors
2170
+ expect(screen.getByText('Home')).toBeInTheDocument();
2171
+ });
2172
+
2173
+ it('matches current path exactly (case-sensitive)', () => {
2174
+ renderWithProviders(
2175
+ <NavigationMenu
2176
+ items={basicNavItems}
2177
+ mode="hierarchical"
2178
+ onNavigate={mockNavigate}
2179
+ currentPath="/dashboard"
2180
+ />
2181
+ );
2182
+
2183
+ // Case-sensitive matching - exact match works
2184
+ const dashboardLink = screen.getByText('Dashboard');
2185
+ expect(dashboardLink.closest('a')).toHaveAttribute('aria-current', 'page');
2186
+ });
2187
+ });
2188
+
2189
+ describe('Ref Forwarding', () => {
2190
+ it('forwards ref correctly in hierarchical mode', () => {
2191
+ const ref = React.createRef<HTMLDivElement>();
2192
+ renderWithProviders(
2193
+ <NavigationMenu
2194
+ ref={ref}
2195
+ items={basicNavItems}
2196
+ mode="hierarchical"
2197
+ onNavigate={mockNavigate}
2198
+ />
2199
+ );
2200
+
2201
+ expect(ref.current).toBeInstanceOf(HTMLElement);
2202
+ expect(ref.current?.tagName).toBe('NAV');
2203
+ });
2204
+
2205
+ it('ref is null when component unmounts', () => {
2206
+ const ref = React.createRef<HTMLDivElement>();
2207
+ const { unmount } = renderWithProviders(
2208
+ <NavigationMenu
2209
+ ref={ref}
2210
+ items={basicNavItems}
2211
+ mode="hierarchical"
2212
+ onNavigate={mockNavigate}
2213
+ />
2214
+ );
2215
+
2216
+ expect(ref.current).toBeInstanceOf(HTMLElement);
2217
+ unmount();
2218
+ expect(ref.current).toBeNull();
2219
+ });
2220
+
2221
+ it('does not forward ref in dropdown mode (Select component handles it)', () => {
2222
+ const ref = React.createRef<HTMLDivElement>();
2223
+ renderWithProviders(
2224
+ <NavigationMenu
2225
+ ref={ref}
2226
+ items={basicNavItems}
2227
+ mode="dropdown"
2228
+ onNavigate={mockNavigate}
2229
+ buttonText="Menu"
2230
+ />
2231
+ );
2232
+
2233
+ // In dropdown mode, ref is not used (Select component handles its own ref)
2234
+ // The ref should still be set to the root element if possible
2235
+ expect(ref.current).toBeDefined();
2236
+ });
2237
+ });
2238
+
2239
+ describe('Disabled State Interactions', () => {
2240
+ it('disables dropdown trigger when disabled prop is true', () => {
2241
+ renderWithProviders(
2242
+ <NavigationMenu
2243
+ items={basicNavItems}
2244
+ onNavigate={mockNavigate}
2245
+ disabled={true}
2246
+ buttonText="Menu"
2247
+ />
2248
+ );
2249
+
2250
+ const trigger = screen.getByRole('combobox');
2251
+ expect(trigger).toBeDisabled();
2252
+ });
2253
+
2254
+ it('allows interaction when not disabled', async () => {
2255
+ const user = userEvent.setup();
2256
+ renderWithProviders(
2257
+ <NavigationMenu
2258
+ items={basicNavItems}
2259
+ onNavigate={mockNavigate}
2260
+ disabled={false}
2261
+ buttonText="Menu"
2262
+ />
2263
+ );
2264
+
2265
+ const trigger = screen.getByRole('combobox');
2266
+ expect(trigger).not.toBeDisabled();
2267
+
2268
+ await user.click(trigger);
2269
+ await waitFor(() => {
2270
+ expect(screen.getByText('Home')).toBeInTheDocument();
2271
+ }, { interval: 10 });
2272
+ });
2273
+
2274
+ it('handles disabled state change dynamically', async () => {
2275
+ const user = userEvent.setup();
2276
+ const { rerender } = renderWithProviders(
2277
+ <NavigationMenu
2278
+ items={basicNavItems}
2279
+ onNavigate={mockNavigate}
2280
+ disabled={false}
2281
+ buttonText="Menu"
2282
+ />
2283
+ );
2284
+
2285
+ const trigger = screen.getByRole('combobox');
2286
+ expect(trigger).not.toBeDisabled();
2287
+
2288
+ // Change to disabled
2289
+ rerender(
2290
+ <NavigationMenu
2291
+ items={basicNavItems}
2292
+ onNavigate={mockNavigate}
2293
+ disabled={true}
2294
+ buttonText="Menu"
2295
+ />
2296
+ );
2297
+
2298
+ expect(trigger).toBeDisabled();
2299
+ });
2300
+ });
2301
+
2302
+ describe('ClassName and Props Spreading', () => {
2303
+ it('applies custom className in dropdown mode', () => {
2304
+ renderWithProviders(
2305
+ <NavigationMenu
2306
+ items={basicNavItems}
2307
+ onNavigate={mockNavigate}
2308
+ className="custom-dropdown-class"
2309
+ buttonText="Menu"
2310
+ />
2311
+ );
2312
+
2313
+ const selectRoot = screen.getByTestId('select-root');
2314
+ expect(selectRoot).toHaveClass('custom-dropdown-class');
2315
+ });
2316
+
2317
+ it('applies custom className in hierarchical mode', () => {
2318
+ renderWithProviders(
2319
+ <NavigationMenu
2320
+ items={basicNavItems}
2321
+ mode="hierarchical"
2322
+ onNavigate={mockNavigate}
2323
+ className="custom-hierarchical-class"
2324
+ />
2325
+ );
2326
+
2327
+ const nav = screen.getByRole('navigation');
2328
+ expect(nav).toHaveClass('custom-hierarchical-class');
2329
+ });
2330
+
2331
+ it('spreads additional props to nav element in hierarchical mode', () => {
2332
+ renderWithProviders(
2333
+ <NavigationMenu
2334
+ items={basicNavItems}
2335
+ mode="hierarchical"
2336
+ onNavigate={mockNavigate}
2337
+ data-testid="custom-nav"
2338
+ aria-label="Custom Navigation"
2339
+ />
2340
+ );
2341
+
2342
+ const nav = screen.getByTestId('custom-nav');
2343
+ expect(nav).toHaveAttribute('aria-label', 'Custom Navigation');
2344
+ });
2345
+
2346
+ it('applies multiple classNames correctly', () => {
2347
+ renderWithProviders(
2348
+ <NavigationMenu
2349
+ items={basicNavItems}
2350
+ mode="hierarchical"
2351
+ onNavigate={mockNavigate}
2352
+ className="class1 class2 class3"
2353
+ />
2354
+ );
2355
+
2356
+ const nav = screen.getByRole('navigation');
2357
+ expect(nav).toHaveClass('class1');
2358
+ expect(nav).toHaveClass('class2');
2359
+ expect(nav).toHaveClass('class3');
2360
+ });
2361
+ });
2362
+
2363
+ describe('Navigation Label', () => {
2364
+ it('uses default navigationLabel when not provided', () => {
2365
+ renderWithProviders(
2366
+ <NavigationMenu
2367
+ items={basicNavItems}
2368
+ mode="hierarchical"
2369
+ onNavigate={mockNavigate}
2370
+ />
2371
+ );
2372
+
2373
+ const nav = screen.getByRole('navigation');
2374
+ expect(nav).toHaveAttribute('aria-label', 'Main navigation');
2375
+ });
2376
+
2377
+ it('uses custom navigationLabel when provided', () => {
2378
+ renderWithProviders(
2379
+ <NavigationMenu
2380
+ items={basicNavItems}
2381
+ mode="hierarchical"
2382
+ onNavigate={mockNavigate}
2383
+ navigationLabel="Custom Navigation Label"
2384
+ />
2385
+ );
2386
+
2387
+ const nav = screen.getByRole('navigation');
2388
+ expect(nav).toHaveAttribute('aria-label', 'Custom Navigation Label');
2389
+ });
2390
+
2391
+ it('navigationLabel does not affect dropdown mode', () => {
2392
+ renderWithProviders(
2393
+ <NavigationMenu
2394
+ items={basicNavItems}
2395
+ mode="dropdown"
2396
+ onNavigate={mockNavigate}
2397
+ navigationLabel="Should not appear"
2398
+ buttonText="Menu"
2399
+ />
2400
+ );
2401
+
2402
+ // navigationLabel is only used in hierarchical mode
2403
+ expect(screen.queryByLabelText('Should not appear')).not.toBeInTheDocument();
2404
+ });
2405
+ });
2406
+
2407
+ describe('Empty and Edge Case Items', () => {
2408
+ it('handles items with empty label', async () => {
2409
+ const user = userEvent.setup();
2410
+ const itemsWithEmptyLabel: NavigationItem[] = [
2411
+ { id: 'empty', label: '', href: '/empty' },
2412
+ { id: 'normal', label: 'Normal', href: '/normal' },
2413
+ ];
2414
+
2415
+ renderWithProviders(
2416
+ <NavigationMenu
2417
+ items={itemsWithEmptyLabel}
2418
+ onNavigate={mockNavigate}
2419
+ buttonText="Menu"
2420
+ />
2421
+ );
2422
+
2423
+ await user.click(screen.getByRole('combobox'));
2424
+ await waitFor(() => {
2425
+ expect(screen.getByText('Normal')).toBeInTheDocument();
2426
+ }, { interval: 10 });
2427
+ });
2428
+
2429
+ it('handles items with null href', async () => {
2430
+ const user = userEvent.setup();
2431
+ const itemsWithNullHref: NavigationItem[] = [
2432
+ { id: 'no-href', label: 'No Href' },
2433
+ { id: 'with-href', label: 'With Href', href: '/with-href' },
2434
+ ];
2435
+
2436
+ renderWithProviders(
2437
+ <NavigationMenu
2438
+ items={itemsWithNullHref}
2439
+ onNavigate={mockNavigate}
2440
+ buttonText="Menu"
2441
+ />
2442
+ );
2443
+
2444
+ await user.click(screen.getByRole('combobox'));
2445
+ await waitFor(() => {
2446
+ const noHrefItem = screen.getByText('No Href');
2447
+ expect(noHrefItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
2448
+ }, { interval: 10 });
2449
+ });
2450
+
2451
+ it('handles items array with duplicate IDs gracefully', async () => {
2452
+ const user = userEvent.setup();
2453
+ const duplicateItems: NavigationItem[] = [
2454
+ { id: 'duplicate', label: 'First', href: '/first' },
2455
+ { id: 'duplicate', label: 'Second', href: '/second' },
2456
+ ];
2457
+
2458
+ renderWithProviders(
2459
+ <NavigationMenu
2460
+ items={duplicateItems}
2461
+ onNavigate={mockNavigate}
2462
+ buttonText="Menu"
2463
+ />
2464
+ );
2465
+
2466
+ await user.click(screen.getByRole('combobox'));
2467
+ await waitFor(() => {
2468
+ // Both items should render (React will use key for rendering)
2469
+ // Use getAllByText since there are duplicates
2470
+ const firstItems = screen.getAllByText('First');
2471
+ const secondItems = screen.getAllByText('Second');
2472
+ expect(firstItems.length).toBeGreaterThan(0);
2473
+ expect(secondItems.length).toBeGreaterThan(0);
2474
+ }, { interval: 10 });
2475
+ });
2476
+
2477
+ it('handles items with very long labels', async () => {
2478
+ const user = userEvent.setup();
2479
+ const longLabelItems: NavigationItem[] = [
2480
+ { id: 'long', label: 'A'.repeat(1000), href: '/long' },
2481
+ ];
2482
+
2483
+ renderWithProviders(
2484
+ <NavigationMenu
2485
+ items={longLabelItems}
2486
+ onNavigate={mockNavigate}
2487
+ buttonText="Menu"
2488
+ />
2489
+ );
2490
+
2491
+ await user.click(screen.getByRole('combobox'));
2492
+ await waitFor(() => {
2493
+ expect(screen.getByText('A'.repeat(1000))).toBeInTheDocument();
2494
+ }, { interval: 10 });
2495
+ });
2496
+
2497
+ it('handles items with special characters in labels', async () => {
2498
+ const user = userEvent.setup();
2499
+ const specialCharItems: NavigationItem[] = [
2500
+ { id: 'special', label: 'Item & <Special> "Chars"', href: '/special' },
2501
+ ];
2502
+
2503
+ renderWithProviders(
2504
+ <NavigationMenu
2505
+ items={specialCharItems}
2506
+ onNavigate={mockNavigate}
2507
+ buttonText="Menu"
2508
+ />
2509
+ );
2510
+
2511
+ await user.click(screen.getByRole('combobox'));
2512
+ await waitFor(() => {
2513
+ expect(screen.getByText('Item & <Special> "Chars"')).toBeInTheDocument();
2514
+ }, { interval: 10 });
2515
+ });
2516
+ });
2517
+
2518
+ describe('Hierarchical Mode Edge Cases', () => {
2519
+ it('handles hierarchical item with empty children array', () => {
2520
+ const itemsWithEmptyChildren: NavigationItem[] = [
2521
+ {
2522
+ id: 'parent',
2523
+ label: 'Parent',
2524
+ children: [],
2525
+ },
2526
+ ];
2527
+
2528
+ renderWithProviders(
2529
+ <NavigationMenu
2530
+ items={itemsWithEmptyChildren}
2531
+ mode="hierarchical"
2532
+ onNavigate={mockNavigate}
2533
+ />
2534
+ );
2535
+
2536
+ // Items with empty children array are rendered as buttons
2537
+ // But the component checks hasChildren which is false for empty array
2538
+ // So it might render as a link or not render at all
2539
+ // Let's check that the component renders without crashing
2540
+ expect(screen.getByText('Parent')).toBeInTheDocument();
2541
+ });
2542
+
2543
+ it('handles hierarchical item with only hidden children', async () => {
2544
+ const user = userEvent.setup();
2545
+ // Note: Hidden children are filtered by useNavigationFiltering hook
2546
+ // So by the time items reach the component, hidden children are already removed
2547
+ // This test verifies the component handles items with no visible children
2548
+ const itemsWithHiddenChildren: NavigationItem[] = [
2549
+ {
2550
+ id: 'parent',
2551
+ label: 'Parent',
2552
+ // Children are already filtered out by the hook, so parent has no children
2553
+ children: [],
2554
+ },
2555
+ ];
2556
+
2557
+ renderWithProviders(
2558
+ <NavigationMenu
2559
+ items={itemsWithHiddenChildren}
2560
+ mode="hierarchical"
2561
+ onNavigate={mockNavigate}
2562
+ />
2563
+ );
2564
+
2565
+ // Parent should render (even without children)
2566
+ expect(screen.getByText('Parent')).toBeInTheDocument();
2567
+
2568
+ // Since children array is empty, parent won't be expandable
2569
+ // The component checks hasChildren which is false for empty array
2570
+ const parentElement = screen.getByText('Parent');
2571
+ expect(parentElement).toBeInTheDocument();
2572
+ });
2573
+
2574
+ it('handles hierarchical item where parent has href and children', async () => {
2575
+ const user = userEvent.setup();
2576
+ const itemsWithHrefAndChildren: NavigationItem[] = [
2577
+ {
2578
+ id: 'parent',
2579
+ label: 'Parent',
2580
+ href: '/parent',
2581
+ children: [
2582
+ { id: 'child', label: 'Child', href: '/child' },
2583
+ ],
2584
+ },
2585
+ ];
2586
+
2587
+ renderWithProviders(
2588
+ <NavigationMenu
2589
+ items={itemsWithHrefAndChildren}
2590
+ mode="hierarchical"
2591
+ onNavigate={mockNavigate}
2592
+ />
2593
+ );
2594
+
2595
+ // Parent should be a button (expandable) even though it has href
2596
+ const parent = screen.getByRole('button', { name: /Parent/i });
2597
+ expect(parent).toBeInTheDocument();
2598
+
2599
+ // Click to expand
2600
+ await user.click(parent);
2601
+ await waitFor(() => {
2602
+ expect(screen.getByText('Child')).toBeInTheDocument();
2603
+ });
2604
+ });
2605
+
2606
+ it('handles deeply nested items (4+ levels)', async () => {
2607
+ const user = userEvent.setup();
2608
+ const deeplyNestedItems: NavigationItem[] = [
2609
+ {
2610
+ id: 'level1',
2611
+ label: 'Level 1',
2612
+ children: [
2613
+ {
2614
+ id: 'level2',
2615
+ label: 'Level 2',
2616
+ children: [
2617
+ {
2618
+ id: 'level3',
2619
+ label: 'Level 3',
2620
+ children: [
2621
+ {
2622
+ id: 'level4',
2623
+ label: 'Level 4',
2624
+ children: [
2625
+ { id: 'level5', label: 'Level 5', href: '/level5' },
2626
+ ],
2627
+ },
2628
+ ],
2629
+ },
2630
+ ],
2631
+ },
2632
+ ],
2633
+ },
2634
+ ];
2635
+
2636
+ renderWithProviders(
2637
+ <NavigationMenu
2638
+ items={deeplyNestedItems}
2639
+ mode="hierarchical"
2640
+ onNavigate={mockNavigate}
2641
+ />
2642
+ );
2643
+
2644
+ // Expand all levels
2645
+ const level1 = screen.getByRole('button', { name: /Level 1/i });
2646
+ await user.click(level1);
2647
+
2648
+ await waitFor(() => {
2649
+ const level2 = screen.getByRole('button', { name: /Level 2/i });
2650
+ expect(level2).toBeInTheDocument();
2651
+ });
2652
+
2653
+ const level2 = screen.getByRole('button', { name: /Level 2/i });
2654
+ await user.click(level2);
2655
+
2656
+ await waitFor(() => {
2657
+ const level3 = screen.getByRole('button', { name: /Level 3/i });
2658
+ expect(level3).toBeInTheDocument();
2659
+ });
2660
+
2661
+ const level3 = screen.getByRole('button', { name: /Level 3/i });
2662
+ await user.click(level3);
2663
+
2664
+ await waitFor(() => {
2665
+ const level4 = screen.getByRole('button', { name: /Level 4/i });
2666
+ expect(level4).toBeInTheDocument();
2667
+ });
2668
+
2669
+ const level4 = screen.getByRole('button', { name: /Level 4/i });
2670
+ await user.click(level4);
2671
+
2672
+ await waitFor(() => {
2673
+ expect(screen.getByText('Level 5')).toBeInTheDocument();
2674
+ });
2675
+ });
2676
+ });
2677
+
2678
+ describe('Button Text Variations', () => {
2679
+ it('uses default buttonText when not provided', () => {
2680
+ renderWithProviders(
2681
+ <NavigationMenu
2682
+ items={basicNavItems}
2683
+ onNavigate={mockNavigate}
2684
+ />
2685
+ );
2686
+
2687
+ expect(screen.getByText('Menu')).toBeInTheDocument();
2688
+ });
2689
+
2690
+ it('uses custom buttonText when provided', () => {
2691
+ renderWithProviders(
2692
+ <NavigationMenu
2693
+ items={basicNavItems}
2694
+ onNavigate={mockNavigate}
2695
+ buttonText="Custom Menu Text"
2696
+ />
2697
+ );
2698
+
2699
+ expect(screen.getByText('Custom Menu Text')).toBeInTheDocument();
2700
+ });
2701
+
2702
+ it('handles empty buttonText', () => {
2703
+ renderWithProviders(
2704
+ <NavigationMenu
2705
+ items={basicNavItems}
2706
+ onNavigate={mockNavigate}
2707
+ buttonText=""
2708
+ />
2709
+ );
2710
+
2711
+ const trigger = screen.getByRole('combobox');
2712
+ expect(trigger).toHaveAttribute('aria-label', '');
2713
+ });
2714
+
2715
+ it('handles buttonText with special characters', () => {
2716
+ renderWithProviders(
2717
+ <NavigationMenu
2718
+ items={basicNavItems}
2719
+ onNavigate={mockNavigate}
2720
+ buttonText="Menu & Navigation <Test>"
2721
+ />
2722
+ );
2723
+
2724
+ expect(screen.getByText('Menu & Navigation <Test>')).toBeInTheDocument();
2725
+ });
2726
+ });
2727
+
2728
+ describe('Select Component Integration', () => {
2729
+ it('handles Select value change correctly', async () => {
2730
+ const user = userEvent.setup();
2731
+ renderWithProviders(
2732
+ <NavigationMenu
2733
+ items={basicNavItems}
2734
+ onNavigate={mockNavigate}
2735
+ buttonText="Menu"
2736
+ />
2737
+ );
2738
+
2739
+ const trigger = screen.getByRole('combobox');
2740
+ await user.click(trigger);
2741
+
2742
+ await waitFor(() => {
2743
+ expect(screen.getByText('Home')).toBeInTheDocument();
2744
+ }, { interval: 10 });
2745
+
2746
+ const homeItem = screen.getByRole('option', { name: 'Home' });
2747
+ await user.click(homeItem);
2748
+
2749
+ expect(mockNavigate).toHaveBeenCalledWith(
2750
+ expect.objectContaining({
2751
+ id: 'home',
2752
+ label: 'Home',
2753
+ href: '/',
2754
+ })
2755
+ );
2756
+ });
2757
+
2758
+ it('disables SelectItem when item has no href', async () => {
2759
+ const user = userEvent.setup();
2760
+ const itemsWithoutHref: NavigationItem[] = [
2761
+ { id: 'action', label: 'Action' },
2762
+ ];
2763
+
2764
+ renderWithProviders(
2765
+ <NavigationMenu
2766
+ items={itemsWithoutHref}
2767
+ onNavigate={mockNavigate}
2768
+ buttonText="Menu"
2769
+ />
2770
+ );
2771
+
2772
+ await user.click(screen.getByRole('combobox'));
2773
+ await waitFor(() => {
2774
+ const actionItem = screen.getByText('Action');
2775
+ expect(actionItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
2776
+ }, { interval: 10 });
2777
+ });
2778
+ });
2779
+
2780
+ describe('Keyboard Navigation Edge Cases', () => {
2781
+ it('handles Space key on hierarchical leaf item without href', async () => {
2782
+ const user = userEvent.setup();
2783
+ const leafWithoutHref: NavigationItem[] = [
2784
+ { id: 'leaf', label: 'Leaf' },
2785
+ ];
2786
+
2787
+ renderWithProviders(
2788
+ <NavigationMenu
2789
+ items={leafWithoutHref}
2790
+ mode="hierarchical"
2791
+ onNavigate={mockNavigate}
2792
+ />
2793
+ );
2794
+
2795
+ const leafLink = screen.getByText('Leaf');
2796
+ leafLink.focus();
2797
+ await user.keyboard(' ');
2798
+
2799
+ // Should not navigate since no href
2800
+ expect(mockNavigate).not.toHaveBeenCalled();
2801
+ });
2802
+
2803
+ it('handles Escape key on non-expanded hierarchical item', async () => {
2804
+ const user = userEvent.setup();
2805
+ renderWithProviders(
2806
+ <NavigationMenu
2807
+ items={hierarchicalNavItems}
2808
+ mode="hierarchical"
2809
+ onNavigate={mockNavigate}
2810
+ />
2811
+ );
2812
+
2813
+ const userManagementButton = screen.getByRole('button', { name: /User Management/i });
2814
+ userManagementButton.focus();
2815
+
2816
+ // Press Escape when not expanded - should not crash
2817
+ await user.keyboard('{Escape}');
2818
+
2819
+ // Should still be collapsed
2820
+ expect(screen.queryByText('All Users')).not.toBeInTheDocument();
2821
+ });
2822
+
2823
+ it('handles Enter key on hierarchical item with children and href', async () => {
2824
+ const user = userEvent.setup();
2825
+ const itemsWithBoth: NavigationItem[] = [
2826
+ {
2827
+ id: 'parent',
2828
+ label: 'Parent',
2829
+ href: '/parent',
2830
+ children: [
2831
+ { id: 'child', label: 'Child', href: '/child' },
2832
+ ],
2833
+ },
2834
+ ];
2835
+
2836
+ renderWithProviders(
2837
+ <NavigationMenu
2838
+ items={itemsWithBoth}
2839
+ mode="hierarchical"
2840
+ onNavigate={mockNavigate}
2841
+ />
2842
+ );
2843
+
2844
+ const parentButton = screen.getByRole('button', { name: /Parent/i });
2845
+ parentButton.focus();
2846
+ await user.keyboard('{Enter}');
2847
+
2848
+ // Should expand (not navigate) since it has children
2849
+ await waitFor(() => {
2850
+ expect(screen.getByText('Child')).toBeInTheDocument();
2851
+ });
2852
+ });
2853
+ });
1537
2854
  });