@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.
- package/CHANGELOG.md +3 -0
- package/audit-tool/audits/02-project-structure.cjs +97 -32
- package/audit-tool/audits/03-architecture.cjs +145 -19
- package/audit-tool/audits/04-code-quality.cjs +86 -1
- package/audit-tool/audits/06-security-rbac.cjs +109 -11
- package/cursor-rules/02-project-structure.mdc +2 -26
- package/cursor-rules/05-styling.mdc +84 -6
- package/cursor-rules/06-security-rbac.mdc +124 -1
- package/dist/{DataTable-6RMSCQJ6.js → DataTable-SAXFG4XI.js} +11 -13
- package/dist/{AuthService-DmfO5rGS.d.ts → InactivityServiceProvider-DHryoh6K.d.ts} +24 -249
- package/dist/UnifiedAuthProvider-BBD2PS3Q.js +7 -0
- package/dist/{UnifiedAuthProvider-CKvHP1MK.d.ts → UnifiedAuthProvider-CiBAl9-s.d.ts} +34 -22
- package/dist/{api-7P7DI652.js → api-F47QJ7FX.js} +3 -3
- package/dist/assets/app-icons/admin_favicon.svg +462 -0
- package/dist/assets/app-icons/base_favicon.svg +85 -0
- package/dist/assets/app-icons/cake_favicon.svg +68 -0
- package/dist/assets/app-icons/core_favicon.svg +256 -0
- package/dist/assets/app-icons/gear_favicon.svg +91 -0
- package/dist/assets/app-icons/medi_favicon.svg +92 -0
- package/dist/assets/app-icons/mint_favicon.svg +83 -0
- package/dist/assets/app-icons/pace_favicon.svg +49 -0
- package/dist/assets/app-icons/pump_favicon.svg +68 -0
- package/dist/assets/app-icons/seed_favicon.svg +91 -0
- package/dist/assets/app-icons/team_favicon.svg +67 -0
- package/dist/assets/app-icons/trac_favicon.svg +112 -0
- package/dist/assets/app-icons/trip_favicon.svg +102 -0
- package/dist/audit-Z6ZZBWLU.js +3 -0
- package/dist/chunk-3GWSPISD.js +61 -0
- package/dist/{chunk-4DDCYDQ3.js → chunk-66R6RLUZ.js} +12 -27
- package/dist/{chunk-FYHN4DD5.js → chunk-7YDC7LMU.js} +80 -8
- package/dist/{chunk-S7DKJPLT.js → chunk-BCTXBU6U.js} +22 -17
- package/dist/{chunk-TTRFSOKR.js → chunk-BTHN5MKC.js} +4 -4
- package/dist/{chunk-A3W6LW53.js → chunk-DDMPHZ3D.js} +6 -18
- package/dist/{chunk-MPBLMWVR.js → chunk-FBZ7U3ID.js} +140 -92
- package/dist/chunk-FN52B75D.js +246 -0
- package/dist/{chunk-5W2A3DRC.js → chunk-JJEYZ3DX.js} +5 -4
- package/dist/chunk-KPYQWGFQ.js +183 -0
- package/dist/{chunk-IUBRCBSY.js → chunk-KSNLMI7N.js} +14 -8
- package/dist/chunk-KYURMOQM.js +977 -0
- package/dist/{chunk-LX6U42O3.js → chunk-LNHFAF4X.js} +160 -58
- package/dist/{chunk-NKHKXPI4.js → chunk-MPY44PWB.js} +683 -627
- package/dist/{chunk-AHU7G2R5.js → chunk-NIU6DPQV.js} +10 -6
- package/dist/{chunk-HF6O3O37.js → chunk-RMLY6KB5.js} +1 -1
- package/dist/{chunk-6GLLNA6U.js → chunk-SACF5YSM.js} +1 -1
- package/dist/{chunk-EURB7QFZ.js → chunk-TFIPNIPE.js} +867 -534
- package/dist/{chunk-OJ4SKRSV.js → chunk-UZNAFKGW.js} +25 -5
- package/dist/chunk-W46INAVW.js +1216 -0
- package/dist/chunk-X5EAU5G7.js +793 -0
- package/dist/{chunk-T5CVK4R3.js → chunk-Y4PF6HIM.js} +110 -64
- package/dist/components.d.ts +8 -86
- package/dist/components.js +21 -55
- package/dist/{database.generated-CcnC_DRc.d.ts → database.generated-DT8JTZiP.d.ts} +12 -12
- package/dist/eslint-rules/rules/05-styling.cjs +507 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +10 -0
- package/dist/{event-CW5YB_2p.d.ts → event-WTAQuGcq.d.ts} +1 -1
- package/dist/{functions-lBy5L2ry.d.ts → functions-DH45k8ec.d.ts} +1 -1
- package/dist/hooks.d.ts +12 -11
- package/dist/hooks.js +69 -44
- package/dist/index.d.ts +380 -32
- package/dist/index.js +46 -32
- package/dist/papaparseLoader-WG2UXQ22.js +7 -0
- package/dist/providers.d.ts +28 -14
- package/dist/providers.js +5 -5
- package/dist/rbac/eslint-rules.js +2 -2
- package/dist/rbac/index.d.ts +58 -214
- package/dist/rbac/index.js +11 -11
- package/dist/theming/runtime.d.ts +9 -3
- package/dist/theming/runtime.js +2 -2
- package/dist/{timezone-BZe_eUxx.d.ts → timezone-K-ptz3HO.d.ts} +22 -23
- package/dist/{types-t9H8qKRw.d.ts → types-BE2sEHKd.d.ts} +1 -1
- package/dist/{types-BeoeWV5I.d.ts → types-CvOPXWWZ.d.ts} +6 -5
- package/dist/{types-DXstZpNI.d.ts → types-D05dCGma.d.ts} +56 -149
- package/dist/types-Dr8sNhER.d.ts +50 -0
- package/dist/types.d.ts +5 -5
- package/dist/{PublicPageProvider-CIGSujI2.d.ts → usePublicPageContext-vxBlEHO9.d.ts} +294 -151
- package/dist/{usePublicRouteParams-MamNgwqe.d.ts → usePublicRouteParams-G3Ks53mk.d.ts} +8 -7
- package/dist/utils.d.ts +301 -137
- package/dist/utils.js +42 -41
- package/dist/{validation-643vUDZW.d.ts → validation-g5n0hDkh.d.ts} +2 -2
- package/docs/api/modules.md +542 -549
- package/docs/api-reference/components.md +5 -5
- package/docs/api-reference/rpc-functions.md +3 -3
- package/docs/implementation-guides/data-tables.md +256 -8
- package/docs/rbac/RBAC_CONTRACT.md +0 -12
- package/docs/standards/2-project-structure-standards.md +12 -74
- package/docs/standards/6-security-rbac-standards.md +222 -7
- package/docs/standards/7-api-tech-stack-standards.md +91 -3
- package/docs/testing/README.md +10 -0
- package/docs/testing/test-setup-for-consumers.md +914 -0
- package/eslint-config-pace-core.cjs +4 -0
- package/package.json +1 -1
- package/scripts/eslint-audit.cjs +110 -11
- package/src/__mocks__/lucide-react.ts +0 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +0 -2
- package/src/__tests__/index.test.ts +532 -0
- package/src/__tests__/integration/UserProfile.test.tsx +1 -1
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +10 -8
- package/src/__tests__/rls-policies.test.ts +3 -2
- package/src/assets/app-icons/admin_favicon.svg +462 -0
- package/src/assets/app-icons/base_favicon.svg +85 -0
- package/src/assets/app-icons/cake_favicon.svg +68 -0
- package/src/assets/app-icons/core_favicon.svg +256 -0
- package/src/assets/app-icons/gear_favicon.svg +91 -0
- package/src/assets/app-icons/index.ts +83 -0
- package/src/assets/app-icons/medi_favicon.svg +92 -0
- package/src/assets/app-icons/mint_favicon.svg +83 -0
- package/src/assets/app-icons/pace_favicon.svg +49 -0
- package/src/assets/app-icons/pump_favicon.svg +68 -0
- package/src/assets/app-icons/seed_favicon.svg +91 -0
- package/src/assets/app-icons/team_favicon.svg +67 -0
- package/src/assets/app-icons/trac_favicon.svg +112 -0
- package/src/assets/app-icons/trip_favicon.svg +102 -0
- package/src/components/AddressField/AddressField.test.tsx +378 -3
- package/src/components/AddressField/AddressField.tsx +2 -2
- package/src/components/AddressField/types.ts +2 -2
- package/src/components/Alert/Alert.test.tsx +35 -25
- package/src/components/Alert/Alert.tsx +8 -8
- package/src/components/AppSwitcher/AppSwitcher.test.tsx +1250 -0
- package/src/components/AppSwitcher/AppSwitcher.tsx +315 -0
- package/src/components/Avatar/Avatar.test.tsx +11 -1
- package/src/components/Avatar/Avatar.tsx +3 -2
- package/src/components/Badge/Badge.test.tsx +11 -1
- package/src/components/Button/Button.test.tsx +13 -3
- package/src/components/Calendar/Calendar.test.tsx +523 -131
- package/src/components/Calendar/Calendar.tsx +107 -488
- package/src/components/Card/Card.test.tsx +220 -249
- package/src/components/Checkbox/Checkbox.test.tsx +58 -174
- package/src/components/ContextSelector/ContextSelector.tsx +3 -3
- package/src/components/ContextSelector/__tests__/ContextSelector.test.tsx +360 -0
- package/src/components/DataTable/DataTable.tsx +2 -2
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +1 -1
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +2 -2
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +1 -1
- package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +485 -0
- package/src/components/DataTable/__tests__/DataTable.test.tsx +2 -2
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +76 -580
- package/src/components/DataTable/__tests__/README.md +1 -1
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +1 -1
- package/src/components/DataTable/__tests__/keyboard.test.tsx +1 -1
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +1 -3
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +0 -6
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +14 -6
- package/src/components/DataTable/components/ActionButtons.tsx +9 -4
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +3 -3
- package/src/components/DataTable/components/ColumnFilter.tsx +2 -7
- package/src/components/DataTable/components/DataTableCore.tsx +44 -52
- package/src/components/DataTable/components/DataTableLayout.tsx +37 -26
- package/src/components/DataTable/components/DataTableModals.tsx +118 -30
- package/src/components/DataTable/components/DataTableToolbar.tsx +2 -2
- package/src/components/DataTable/components/EditFields.tsx +6 -47
- package/src/components/DataTable/components/EditableRow.tsx +8 -8
- package/src/components/DataTable/components/EmptyState.tsx +6 -3
- package/src/components/DataTable/components/FilterRow.tsx +18 -11
- package/src/components/DataTable/components/GroupingDropdown.tsx +0 -1
- package/src/components/DataTable/components/ImportModal.tsx +305 -133
- package/src/components/DataTable/components/LoadingState.tsx +2 -2
- package/src/components/DataTable/components/PaginationControls.tsx +0 -4
- package/src/components/DataTable/components/RowComponent.tsx +42 -22
- package/src/components/DataTable/components/UnifiedTableBody.tsx +52 -12
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +51 -463
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +122 -116
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +40 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +9 -137
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +57 -17
- package/src/components/DataTable/components/__tests__/DataTableCore.test.tsx +792 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +24 -65
- package/src/components/DataTable/components/__tests__/DataTableLayout.test.tsx +467 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +8 -125
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +528 -56
- package/src/components/DataTable/components/__tests__/EditFields.test.tsx +526 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +1 -68
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +8 -25
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +3 -62
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +9 -14
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +50 -186
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +39 -97
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +13 -103
- package/src/components/DataTable/components/__tests__/RowComponent.test.tsx +629 -0
- package/src/components/DataTable/components/__tests__/SortIndicator.test.tsx +135 -0
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +31 -171
- package/src/components/DataTable/components/__tests__/cellValueUtils.test.ts +453 -0
- package/src/components/DataTable/components/hooks/useImportModalFocus.test.ts +184 -0
- package/src/components/DataTable/components/hooks/usePermissionTracking.test.ts +381 -0
- package/src/components/DataTable/context/DataTableContext.tsx +9 -10
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +12 -26
- package/src/components/DataTable/core/ColumnFactory.ts +3 -3
- package/src/components/DataTable/core/ColumnManager.ts +0 -1
- package/src/components/DataTable/core/DataManager.ts +4 -2
- package/src/components/DataTable/core/LocalDataAdapter.ts +1 -1
- package/src/components/DataTable/core/PluginRegistry.ts +2 -2
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +114 -2
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +103 -5
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +57 -0
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +63 -0
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +42 -9
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +29 -7
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +58 -4
- package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +16 -21
- package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +93 -4
- package/src/components/DataTable/hooks/__tests__/useDataTableConfiguration.test.ts +227 -54
- package/src/components/DataTable/hooks/__tests__/useDataTableDataPipeline.test.ts +215 -62
- package/src/components/DataTable/hooks/__tests__/useDataTablePermissions.test.ts +217 -39
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +101 -6
- package/src/components/DataTable/hooks/__tests__/useEffectiveColumnOrder.test.ts +157 -27
- package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +80 -0
- package/src/components/DataTable/hooks/__tests__/useKeyboardNavigation.test.ts +787 -0
- package/src/components/DataTable/hooks/__tests__/useServerSideDataEffect.test.ts +258 -0
- package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +298 -23
- package/src/components/DataTable/hooks/__tests__/useTableHandlers.test.ts +440 -0
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +1 -1
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +11 -22
- package/src/components/DataTable/hooks/useDataTableState.ts +20 -24
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +5 -5
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +13 -1
- package/src/components/DataTable/hooks/useTableColumns.ts +36 -38
- package/src/components/DataTable/hooks/useTableHandlers.ts +8 -20
- package/src/components/DataTable/index.ts +24 -2
- package/src/components/DataTable/types.ts +6 -3
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +3 -67
- package/src/components/DataTable/utils/__tests__/aggregationUtils.test.ts +288 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +3 -60
- package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +1 -1
- package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +9 -21
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +102 -86
- package/src/components/DataTable/utils/__tests__/paginationUtils.test.ts +593 -0
- package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +33 -49
- package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +208 -0
- package/src/components/DataTable/utils/a11yUtils.ts +1 -1
- package/src/components/DataTable/utils/aggregationUtils.ts +5 -5
- package/src/components/DataTable/utils/errorHandling.ts +3 -1
- package/src/components/DataTable/utils/exportUtils.ts +1 -1
- package/src/components/DataTable/utils/flexibleImport.ts +2 -2
- package/src/components/DataTable/utils/hierarchicalSorting.ts +3 -3
- package/src/components/DataTable/utils/paginationUtils.ts +1 -1
- package/src/components/DataTable/utils/performanceUtils.ts +1 -1
- package/src/components/DataTable/utils/selectFieldUtils.ts +127 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +17 -24
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.test.tsx +2 -15
- package/src/components/DateTimeField/DateTimeField.tsx +1 -1
- package/src/components/Dialog/Dialog.test.tsx +2007 -407
- package/src/components/Dialog/Dialog.tsx +97 -192
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +2 -62
- package/src/components/ErrorBoundary/ErrorBoundaryContext.context.ts +17 -0
- package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +2 -45
- package/src/components/ErrorBoundary/ErrorBoundaryContext.types.ts +41 -0
- package/src/components/ErrorBoundary/index.ts +3 -4
- package/src/components/ErrorBoundary/useErrorBoundaryContext.ts +20 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -222
- package/src/components/FileDisplay/FileDisplay.tsx +14 -12
- package/src/components/FileDisplay/index.tsx +1 -1
- package/src/components/FileUpload/FileUpload.test.tsx +54 -18
- package/src/components/FileUpload/FileUpload.tsx +10 -7
- package/src/components/FileUpload/index.tsx +1 -1
- package/src/components/Footer/Footer.test.tsx +33 -114
- package/src/components/Form/Form.test.tsx +388 -68
- package/src/components/Form/Form.tsx +57 -42
- package/src/components/Header/Header.test.tsx +645 -154
- package/src/components/Header/Header.tsx +52 -43
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +35 -76
- package/src/components/Input/Input.test.tsx +34 -120
- package/src/components/Label/Label.test.tsx +47 -46
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +9 -12
- package/src/components/LoginForm/LoginForm.test.tsx +0 -1
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +1399 -82
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/__tests__/useNavigationFiltering.test.ts +1934 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +5 -15
- package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +1307 -0
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +47 -46
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +81 -38
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +87 -66
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +245 -39
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +14 -20
- package/src/components/PaceAppLayout/README.md +0 -9
- package/src/components/PaceAppLayout/test-setup.tsx +15 -9
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +759 -3
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +2 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +1 -1
- package/src/components/Progress/Progress.test.tsx +127 -1
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +1196 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +24 -6
- package/src/components/PublicLayout/PublicLayout.test.tsx +1435 -14
- package/src/components/PublicLayout/PublicPageContext.ts +28 -0
- package/src/components/PublicLayout/PublicPageLayout.tsx +6 -6
- package/src/components/PublicLayout/PublicPageProvider.tsx +2 -41
- package/src/components/PublicLayout/usePublicPageContext.ts +36 -0
- package/src/components/Select/Select.test.tsx +46 -9
- package/src/components/Select/Select.tsx +31 -24
- package/src/components/Select/__tests__/context.test.tsx +56 -0
- package/src/components/Select/hooks/__tests__/useSelectEvents.test.ts +279 -0
- package/src/components/Select/hooks/__tests__/useSelectSearch.test.tsx +295 -0
- package/src/components/Select/hooks/__tests__/useSelectState.test.ts +254 -0
- package/src/components/Select/hooks/useSelectState.ts +16 -16
- package/src/components/Select/types.ts +3 -0
- package/src/components/Select/utils/__tests__/text.test.tsx +104 -0
- package/src/components/SessionRestorationLoader/SessionRestorationLoader.test.tsx +28 -112
- package/src/components/Switch/Switch.test.tsx +57 -153
- package/src/components/Table/Table.test.tsx +47 -317
- package/src/components/Tabs/Tabs.tsx +3 -3
- package/src/components/Textarea/Textarea.test.tsx +11 -38
- package/src/components/Toast/Toast.test.tsx +78 -569
- package/src/components/Tooltip/Tooltip.test.tsx +4 -21
- package/src/components/UserMenu/UserMenu.test.tsx +1 -21
- package/src/components/UserMenu/UserMenu.tsx +3 -6
- package/src/components/__tests__/index.test.ts +346 -0
- package/src/components/index.ts +12 -1
- package/src/constants/__tests__/performance.test.ts +91 -0
- package/src/hooks/__tests__/ServiceHooks.test.tsx +239 -129
- package/src/hooks/__tests__/hooks.integration.test.tsx +4 -3
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +1 -1
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +88 -29
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +282 -98
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +53 -109
- package/src/hooks/__tests__/useDataTableState.test.ts +143 -49
- package/src/hooks/__tests__/useDebounce.unit.test.ts +94 -19
- package/src/hooks/__tests__/useEvents.unit.test.ts +100 -125
- package/src/hooks/__tests__/useFileDisplay.test.ts +540 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1 -4
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +27 -247
- package/src/hooks/__tests__/useFileUrlCache.test.ts +246 -56
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +442 -68
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +345 -560
- package/src/hooks/__tests__/useFormDialog.test.ts +51 -222
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +1 -1
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +1 -4
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +0 -1
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +1 -1
- package/src/hooks/__tests__/usePermissionCache.test.ts +506 -0
- package/src/hooks/__tests__/usePreventTabReload.test.ts +255 -36
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +17 -8
- package/src/hooks/__tests__/usePublicEvent.test.ts +16 -24
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +12 -4
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +3 -6
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +1 -2
- package/src/hooks/__tests__/useQueryCache.test.ts +313 -66
- package/src/hooks/__tests__/useSessionDraft.test.ts +496 -103
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +2 -2
- package/src/hooks/__tests__/useStorage.unit.test.ts +72 -102
- package/src/hooks/__tests__/useToast.test.ts +413 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +1 -1
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +175 -21
- package/src/hooks/index.ts +13 -1
- package/src/hooks/public/usePublicEvent.test.ts +304 -0
- package/src/hooks/public/usePublicEvent.ts +11 -11
- package/src/hooks/public/usePublicEventLogo.test.ts +655 -120
- package/src/hooks/public/usePublicEventLogo.ts +2 -2
- package/src/hooks/public/usePublicFileDisplay.test.ts +723 -0
- package/src/hooks/public/usePublicFileDisplay.ts +79 -49
- package/src/hooks/public/usePublicRouteParams.test.ts +595 -0
- package/src/hooks/public/usePublicRouteParams.ts +2 -2
- package/src/hooks/services/useAuthService.ts +1 -1
- package/src/hooks/services/useEventService.ts +1 -1
- package/src/hooks/useAccessibleApps.test.ts +400 -0
- package/src/hooks/useAccessibleApps.ts +264 -0
- package/src/hooks/useAddressAutocomplete.test.ts +165 -42
- package/src/hooks/useAddressAutocomplete.ts +41 -28
- package/src/hooks/useAppConfig.ts +13 -3
- package/src/hooks/useDataTablePerformance.ts +13 -12
- package/src/hooks/useDataTableState.ts +5 -5
- package/src/hooks/useEventTheme.test.ts +66 -17
- package/src/hooks/useEventTheme.ts +1 -1
- package/src/hooks/useEvents.ts +8 -1
- package/src/hooks/useFileDisplay.ts +66 -33
- package/src/hooks/useFileReference.test.ts +365 -87
- package/src/hooks/useFileReference.ts +2 -6
- package/src/hooks/useFileUrlCache.ts +4 -1
- package/src/hooks/useFormDialog.ts +2 -2
- package/src/hooks/useInactivityTracker.ts +3 -3
- package/src/hooks/useOrganisationPermissions.test.ts +1 -2
- package/src/hooks/useOrganisationPermissions.ts +1 -4
- package/src/hooks/useOrganisationSecurity.test.ts +1 -30
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/useOrganisations.ts +1 -1
- package/src/hooks/usePerformanceMonitor.ts +1 -1
- package/src/hooks/usePermissionCache.ts +2 -6
- package/src/hooks/useQueryCache.ts +7 -7
- package/src/hooks/useSessionDraft.ts +14 -11
- package/src/hooks/useSessionRestoration.ts +1 -1
- package/src/hooks/useStorage.ts +75 -40
- package/src/hooks/useToast.ts +2 -2
- package/src/hooks/useZodForm.ts +3 -3
- package/src/icons/__tests__/index.test.ts +133 -0
- package/src/icons/index.ts +1 -1
- package/src/index.ts +43 -4
- package/src/providers/OrganisationProvider.test.tsx +40 -0
- package/src/providers/OrganisationProvider.tsx +5 -5
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +7 -12
- package/src/providers/__tests__/AuthProvider.test.tsx +22 -91
- package/src/providers/__tests__/EventProvider.test.tsx +16 -80
- package/src/providers/__tests__/InactivityProvider.test.tsx +29 -173
- package/src/providers/__tests__/OrganisationProvider.test.tsx +4 -5
- package/src/providers/__tests__/OrganisationProvider.wrapper.test.tsx +591 -0
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +80 -196
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +40 -133
- package/src/providers/__tests__/index.test.ts +138 -0
- package/src/providers/services/AuthServiceContext.ts +27 -0
- package/src/providers/services/AuthServiceProvider.tsx +81 -20
- package/src/providers/services/EventServiceContext.ts +25 -0
- package/src/providers/services/EventServiceProvider.tsx +11 -20
- package/src/providers/services/InactivityServiceContext.ts +25 -0
- package/src/providers/services/InactivityServiceProvider.tsx +7 -17
- package/src/providers/services/OrganisationServiceContext.ts +25 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +7 -17
- package/src/providers/services/UnifiedAuthContext.ts +99 -0
- package/src/providers/services/UnifiedAuthProvider.test.tsx +212 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +38 -143
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +61 -95
- package/src/providers/services/__tests__/AuthServiceProvider.test.tsx +638 -0
- package/src/providers/services/__tests__/EventServiceProvider.test.tsx +839 -0
- package/src/providers/services/__tests__/InactivityServiceProvider.test.tsx +662 -0
- package/src/providers/services/__tests__/OrganisationServiceProvider.test.tsx +440 -0
- package/src/providers/services/__tests__/UnifiedAuthProvider.advanced.test.tsx +435 -0
- package/src/providers/services/__tests__/UnifiedAuthProvider.appId.test.tsx +408 -0
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +55 -48
- package/src/providers/services/__tests__/contexts.test.tsx +281 -0
- package/src/providers/services/__tests__/useUnifiedAuth.test.tsx +251 -0
- package/src/providers/services/useUnifiedAuth.ts +29 -0
- package/src/rbac/README.md +5 -5
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +9 -14
- package/src/rbac/__tests__/audit-batched.test.ts +550 -0
- package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +1 -14
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +43 -12
- package/src/rbac/__tests__/cache-invalidation.test.ts +8 -14
- package/src/rbac/__tests__/engine.comprehensive.test.ts +2 -7
- package/src/rbac/__tests__/index.test.ts +107 -0
- package/src/rbac/__tests__/performance.test.ts +451 -0
- package/src/rbac/__tests__/rbac-core.test.tsx +2 -2
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +0 -5
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +1 -7
- package/src/rbac/__tests__/rbac-functions.test.ts +0 -1
- package/src/rbac/__tests__/rbac-integration.test.ts +0 -1
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +21 -32
- package/src/rbac/adapters.test.tsx +654 -0
- package/src/rbac/adapters.tsx +24 -9
- package/src/rbac/api.test.ts +13 -217
- package/src/rbac/api.ts +85 -16
- package/src/rbac/audit-batched.ts +5 -4
- package/src/rbac/audit.test.ts +225 -28
- package/src/rbac/audit.ts +22 -17
- package/src/rbac/cache-invalidation.ts +18 -15
- package/src/rbac/cache.test.ts +123 -63
- package/src/rbac/cache.ts +3 -4
- package/src/rbac/components/AccessDenied.tsx +20 -18
- package/src/rbac/components/NavigationGuard.tsx +10 -8
- package/src/rbac/components/PagePermissionGuard.tsx +27 -25
- package/src/rbac/components/__tests__/AccessDenied.test.tsx +324 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +242 -71
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +20 -37
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +18 -17
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +452 -129
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -13
- package/src/rbac/config.test.ts +131 -48
- package/src/rbac/config.ts +11 -8
- package/src/rbac/docs/event-based-apps.md +26 -13
- package/src/rbac/engine.test.ts +496 -146
- package/src/rbac/engine.ts +53 -13
- package/src/rbac/errors.test.ts +99 -87
- package/src/rbac/eslint-rules.js +2 -2
- package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +0 -5
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +601 -1
- package/src/rbac/hooks/permissions/__tests__/useAccessLevel.test.ts +622 -0
- package/src/rbac/hooks/permissions/__tests__/useCan.test.ts +798 -0
- package/src/rbac/hooks/permissions/__tests__/useMultiplePermissions.test.ts +843 -0
- package/src/rbac/hooks/permissions/__tests__/usePermissions.test.ts +545 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +7 -8
- package/src/rbac/hooks/permissions/useCan.ts +12 -10
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +57 -8
- package/src/rbac/hooks/permissions/usePermissions.ts +15 -14
- package/src/rbac/hooks/useCan.test.ts +319 -3
- package/src/rbac/hooks/usePermissions.test.ts +426 -0
- package/src/rbac/hooks/usePermissions.ts +5 -7
- package/src/rbac/hooks/useRBAC.test.ts +1669 -2
- package/src/rbac/hooks/useRBAC.ts +7 -11
- package/src/rbac/hooks/useResolvedScope.test.ts +442 -5
- package/src/rbac/hooks/useResolvedScope.ts +4 -1
- package/src/rbac/hooks/useResourcePermissions.test.ts +538 -1
- package/src/rbac/hooks/useResourcePermissions.ts +9 -7
- package/src/rbac/hooks/useRoleManagement.test.ts +659 -1
- package/src/rbac/hooks/useRoleManagement.ts +16 -12
- package/src/rbac/hooks/useSecureSupabase.ts +11 -12
- package/src/rbac/index.ts +32 -32
- package/src/rbac/permissions.test.ts +149 -68
- package/src/rbac/permissions.ts +0 -3
- package/src/rbac/request-deduplication.test.ts +347 -0
- package/src/rbac/secureClient.test.ts +112 -159
- package/src/rbac/secureClient.ts +46 -26
- package/src/rbac/security.test.ts +125 -44
- package/src/rbac/security.ts +7 -6
- package/src/rbac/types.test.ts +236 -0
- package/src/rbac/types.ts +7 -5
- package/src/rbac/utils/__tests__/clientSecurity.test.ts +192 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +1 -3
- package/src/rbac/utils/__tests__/deep-equal.test.ts +23 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +10 -57
- package/src/rbac/utils/clientSecurity.ts +6 -4
- package/src/rbac/utils/contextValidator.ts +1 -2
- package/src/rbac/utils/eventContext.ts +2 -2
- package/src/services/AuthService.ts +13 -11
- package/src/services/EventService.ts +4 -5
- package/src/services/OrganisationService.ts +13 -30
- package/src/services/__tests__/AuthService.edge-cases.test.ts +746 -0
- package/src/services/__tests__/AuthService.restoreSession.test.ts +23 -3
- package/src/services/__tests__/AuthService.test.ts +4 -8
- package/src/services/__tests__/BaseService.edge-cases.test.ts +506 -0
- package/src/services/__tests__/BaseService.test.ts +49 -0
- package/src/services/__tests__/EventService.edge-cases.test.ts +633 -0
- package/src/services/__tests__/EventService.eventColours.test.ts +0 -12
- package/src/services/__tests__/EventService.test.ts +0 -7
- package/src/services/__tests__/InactivityService.edge-cases.test.ts +492 -0
- package/src/services/__tests__/InactivityService.lifecycle.test.ts +0 -5
- package/src/services/__tests__/OrganisationService.edge-cases.test.ts +633 -0
- package/src/services/base/BaseService.test.ts +214 -0
- package/src/services/interfaces/IOrganisationService.ts +0 -1
- package/src/services/interfaces/__tests__/IAuthService.test.ts +190 -0
- package/src/services/interfaces/__tests__/IEventService.test.ts +176 -0
- package/src/services/interfaces/__tests__/IInactivityService.test.ts +183 -0
- package/src/services/interfaces/__tests__/IOrganisationService.test.ts +207 -0
- package/src/styles/core.css +1 -0
- package/src/theming/__tests__/runtime.test.ts +29 -94
- package/src/theming/parseEventColours.ts +18 -9
- package/src/theming/runtime.ts +1 -5
- package/src/types/__tests__/core.test.ts +397 -0
- package/src/types/__tests__/database-generated.test.ts +78 -0
- package/src/types/__tests__/file-reference.test.ts +270 -366
- package/src/types/__tests__/guards.test.ts +26 -26
- package/src/types/__tests__/index.test.ts +265 -0
- package/src/types/__tests__/type-validation.test.ts +3 -3
- package/src/types/__tests__/validation.test.ts +0 -2
- package/src/types/auth.ts +0 -1
- package/src/types/database.generated.ts +9 -9
- package/src/types/event.ts +1 -1
- package/src/types/rpc-responses.ts +33 -0
- package/src/types/supabase.ts +1 -2
- package/src/types/vitest-globals.d.ts +1 -1
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +64 -77
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +13 -0
- package/src/utils/__tests__/formatDate.unit.test.ts +1 -1
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +0 -1
- package/src/utils/__tests__/logger.unit.test.ts +1 -1
- package/src/utils/__tests__/performanceBenchmark.test.ts +1 -2
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +48 -13
- package/src/utils/__tests__/request-deduplication.test.ts +349 -0
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +0 -1
- package/src/utils/__tests__/timezone.test.ts +1 -1
- package/src/utils/__tests__/validation.unit.test.ts +1 -2
- package/src/utils/__tests__/validationUtils.unit.test.ts +1 -1
- package/src/utils/app/appConfig.test.ts +235 -0
- package/src/utils/app/appIdResolver.test.ts +188 -20
- package/src/utils/app/appNameResolver.test.ts +18 -10
- package/src/utils/app/appNameResolver.ts +11 -9
- package/src/utils/app/appPortMap.test.ts +125 -0
- package/src/utils/app/appPortMap.ts +51 -0
- package/src/utils/app/buildAppUrl.test.ts +273 -0
- package/src/utils/app/buildAppUrl.ts +114 -0
- package/src/utils/audit/audit.test.ts +354 -39
- package/src/utils/context/organisationContext.test.ts +10 -4
- package/src/utils/context/organisationContext.ts +5 -5
- package/src/utils/context/sessionTracking.test.ts +354 -0
- package/src/utils/core/__tests__/cn.test.ts +66 -0
- package/src/utils/core/__tests__/debugLogger.test.ts +113 -0
- package/src/utils/core/__tests__/logger.test.ts +217 -0
- package/src/utils/core/debugLogger.ts +15 -8
- package/src/utils/core/logger.ts +20 -16
- package/src/utils/device/deviceFingerprint.test.ts +8 -5
- package/src/utils/device/deviceFingerprint.ts +3 -3
- package/src/utils/dynamic/__tests__/dynamicUtils.test.ts +185 -0
- package/src/utils/dynamic/__tests__/lazyLoad.test.tsx +156 -0
- package/src/utils/dynamic/createLazyComponent.tsx +38 -0
- package/src/utils/dynamic/dynamicUtils.ts +6 -6
- package/src/utils/dynamic/lazyLoad.tsx +8 -36
- package/src/utils/dynamic/papaparseLoader.ts +7 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +583 -145
- package/src/utils/file-reference/index.ts +0 -1
- package/src/utils/formatting/formatDate.test.ts +22 -148
- package/src/utils/formatting/formatDateTime.test.ts +41 -119
- package/src/utils/formatting/formatDateTimeTimezone.test.ts +40 -84
- package/src/utils/formatting/formatNumber.test.ts +259 -0
- package/src/utils/formatting/formatTime.test.ts +36 -128
- package/src/utils/formatting/formatting.ts +1 -1
- package/src/utils/google-places/googlePlacesUtils.test.ts +72 -3
- package/src/utils/google-places/googlePlacesUtils.ts +15 -2
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +58 -1
- package/src/utils/google-places/loadGoogleMapsScript.ts +2 -1
- package/src/utils/index.ts +52 -11
- package/src/utils/location/location.test.ts +18 -115
- package/src/utils/performance/__tests__/bundleAnalysis.test.ts +148 -0
- package/src/utils/performance/__tests__/performanceBenchmark.test.ts +251 -0
- package/src/utils/performance/__tests__/performanceBudgets.test.ts +241 -0
- package/src/utils/performance/bundleAnalysis.ts +16 -22
- package/src/utils/performance/performanceBenchmark.ts +12 -4
- package/src/utils/performance/performanceBudgets.ts +9 -6
- package/src/utils/permissions/__tests__/permissionTypes.test.ts +149 -0
- package/src/utils/permissions/permissionUtils.test.ts +20 -42
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +180 -9
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +164 -16
- package/src/utils/persistence/sensitiveFieldDetection.ts +2 -2
- package/src/utils/request-deduplication.ts +6 -4
- package/src/utils/security/auth-utils.ts +7 -7
- package/src/utils/security/secureDataAccess.test.ts +22 -191
- package/src/utils/security/secureErrors.test.ts +163 -0
- package/src/utils/security/secureStorage.test.ts +156 -0
- package/src/utils/security/secureStorage.ts +1 -1
- package/src/utils/security/security.test.ts +204 -0
- package/src/utils/security/securityMonitor.test.ts +90 -0
- package/src/utils/security/securityMonitor.ts +1 -1
- package/src/utils/storage/__tests__/config.unit.test.ts +239 -0
- package/src/utils/storage/__tests__/index.unit.test.ts +64 -12
- package/src/utils/storage/helpers.test.ts +757 -430
- package/src/utils/storage/helpers.ts +1 -2
- package/src/utils/storage/{index.ts → storageUtils.ts} +1 -36
- package/src/utils/storage/types.ts +2 -2
- package/src/utils/supabase/createBaseClient.test.ts +201 -0
- package/src/utils/supabase/createBaseClient.ts +27 -8
- package/src/utils/timezone/timezone.test.ts +25 -43
- package/src/utils/validation/__tests__/common.test.ts +115 -0
- package/src/utils/validation/__tests__/csrf.test.ts +65 -0
- package/src/utils/validation/__tests__/htmlSanitization.unit.test.ts +27 -7
- package/src/utils/validation/__tests__/passwordSchema.test.ts +164 -0
- package/src/utils/validation/__tests__/schema.test.ts +127 -0
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +76 -3
- package/src/utils/validation/__tests__/user.test.ts +173 -0
- package/src/utils/validation/__tests__/validation.test.ts +197 -0
- package/src/utils/validation/__tests__/validationUtils.test.ts +265 -43
- package/src/utils/validation/htmlSanitization.ts +27 -31
- package/src/utils/validation/schema.ts +6 -3
- package/src/utils/validation/sqlInjectionProtection.ts +2 -2
- package/src/vite-env.d.ts +6 -0
- package/dist/DataTable-DRUIgtUH.d.ts +0 -166
- package/dist/UnifiedAuthProvider-7SNDOWYD.js +0 -7
- package/dist/audit-MYQXYZFU.js +0 -3
- package/dist/chunk-7ILTDCL2.js +0 -80
- package/dist/chunk-EF2UGZWY.js +0 -611
- package/dist/chunk-FEJLJNWA.js +0 -181
- package/dist/chunk-GS5672WG.js +0 -2003
- package/dist/chunk-S6ZQKDY6.js +0 -62
- package/dist/chunk-Z2FNRKF3.js +0 -994
- package/dist/useToast-AyaT-x7p.d.ts +0 -68
- package/src/components/DataTable/components/index.ts +0 -16
- package/src/components/DataTable/core/index.ts +0 -1
- package/src/components/DataTable/hooks/index.ts +0 -13
- package/src/components/DataTable/utils/index.ts +0 -9
- package/src/components/PublicLayout/index.ts +0 -32
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/public/index.ts +0 -36
- package/src/hooks/usePermissionCache.test.ts +0 -536
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +0 -82
- package/src/rbac/audit-enhanced.ts +0 -384
- package/src/rbac/compliance/database-validator.ts +0 -165
- package/src/rbac/compliance/index.ts +0 -48
- package/src/rbac/compliance/pattern-detector.ts +0 -553
- package/src/rbac/compliance/quick-fix-suggestions.ts +0 -209
- package/src/rbac/compliance/runtime-compliance.ts +0 -99
- package/src/rbac/compliance/setup-validator.ts +0 -131
- package/src/rbac/components/index.ts +0 -26
- package/src/rbac/hooks/index.ts +0 -34
- package/src/rbac/hooks/permissions/index.ts +0 -4
- package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -490
- package/src/utils/app/appNameResolver.simple.test.ts +0 -212
- package/src/utils/google-places/index.ts +0 -26
- package/src/utils/location/index.ts +0 -16
- package/src/utils/storage/__tests__/helpers.unit.test.ts +0 -332
- package/src/utils/timezone/index.ts +0 -17
- 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
|
|
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('
|
|
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
|
|
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.
|
|
1413
|
-
mockUseRBAC.
|
|
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
|
});
|