@jmruthers/pace-core 0.6.9 → 0.6.11
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 +21 -0
- package/audit-tool/00-dependencies.cjs +46 -13
- package/audit-tool/audits/01-pace-core-compliance.cjs +96 -21
- package/audit-tool/audits/02-project-structure.cjs +74 -2
- package/audit-tool/audits/03-architecture.cjs +220 -20
- package/audit-tool/audits/04-code-quality.cjs +95 -3
- package/audit-tool/audits/05-styling.cjs +19 -7
- package/audit-tool/audits/06-security-rbac.cjs +214 -25
- package/audit-tool/audits/07-api-tech-stack.cjs +31 -15
- package/audit-tool/audits/08-testing-documentation.cjs +11 -3
- package/audit-tool/audits/09-operations.cjs +19 -7
- package/audit-tool/index.cjs +22 -11
- package/audit-tool/utils/report-utils.cjs +4 -0
- package/cursor-rules/01-pace-core-compliance.mdc +1 -0
- package/cursor-rules/02-project-structure.mdc +3 -26
- package/cursor-rules/03-architecture.mdc +3 -1
- package/cursor-rules/04-code-quality.mdc +1 -0
- package/cursor-rules/05-styling.mdc +120 -8
- package/cursor-rules/06-security-rbac.mdc +126 -2
- package/cursor-rules/07-api-tech-stack.mdc +1 -0
- package/cursor-rules/08-testing-documentation.mdc +1 -0
- package/cursor-rules/09-operations.mdc +1 -0
- package/dist/DataTable-EFYP2QLE.js +16 -0
- package/dist/InactivityServiceProvider-BbxwwDz1.d.ts +308 -0
- package/dist/UnifiedAuthProvider-Bkt_tzdS.d.ts +183 -0
- package/dist/api-BZR2CYXL.js +5 -0
- package/dist/api-result-USV1Czr-.d.ts +51 -0
- 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-HI2DHUVU.js +4 -0
- package/dist/auth-JvdRVaud.d.ts +49 -0
- package/dist/chunk-2DL2WSOE.js +327 -0
- package/dist/chunk-2OEVOGGR.js +9598 -0
- package/dist/chunk-44CNXN4P.js +15 -0
- package/dist/chunk-4R3T5ENU.js +2943 -0
- package/dist/chunk-7A6IMHH2.js +2321 -0
- package/dist/chunk-BTHN5MKC.js +121 -0
- package/dist/chunk-CU2BU2MQ.js +2 -0
- package/dist/chunk-D6BMFMQZ.js +200 -0
- package/dist/chunk-DDMPHZ3D.js +58 -0
- package/dist/chunk-ENLXB7GP.js +721 -0
- package/dist/chunk-J2KQK6DG.js +2159 -0
- package/dist/chunk-KJXRL3XE.js +6434 -0
- package/dist/chunk-L5LFKKLJ.js +61 -0
- package/dist/chunk-PCSHBLPB.js +811 -0
- package/dist/chunk-QRYSEPHB.js +429 -0
- package/dist/chunk-RMLY6KB5.js +187 -0
- package/dist/chunk-SACF5YSM.js +31 -0
- package/dist/chunk-UZNAFKGW.js +125 -0
- package/dist/chunk-V7FTM2LU.js +1080 -0
- package/dist/chunk-WY6Y7KC3.js +264 -0
- package/dist/chunk-XOJME5T7.js +407 -0
- package/dist/chunk-XPFVT3GN.js +492 -0
- package/dist/chunk-YFTFFJIV.js +529 -0
- package/dist/chunk-YYTWKVHO.js +1334 -0
- package/dist/components.d.ts +12 -89
- package/dist/components.js +23 -55
- package/dist/database.generated-qkdoiVrJ.d.ts +9441 -0
- package/dist/eslint-rules/index.cjs +3 -0
- package/dist/eslint-rules/rules/03-architecture.cjs +74 -0
- package/dist/eslint-rules/rules/05-styling.cjs +507 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +84 -0
- package/dist/event-BfCox3N2.d.ts +265 -0
- package/dist/file-reference-DU1hcawx.d.ts +164 -0
- package/dist/functions-DH45k8ec.d.ts +208 -0
- package/dist/hooks.d.ts +28 -14
- package/dist/hooks.js +90 -56
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +392 -155
- package/dist/index.js +337 -347
- package/dist/pagination-BW1mqywp.d.ts +201 -0
- package/dist/papaparseLoader-WG2UXQ22.js +7 -0
- package/dist/providers.d.ts +29 -14
- package/dist/providers.js +7 -5
- package/dist/rbac/eslint-rules.js +2 -2
- package/dist/rbac/index.d.ts +180 -351
- package/dist/rbac/index.js +13 -11
- package/dist/theming/runtime.d.ts +28 -5
- package/dist/theming/runtime.js +2 -2
- package/dist/timezone-BTWWXKVY.d.ts +696 -0
- package/dist/types-BE2sEHKd.d.ts +55 -0
- package/dist/types-CvOPXWWZ.d.ts +111 -0
- package/dist/types-Dr8sNhER.d.ts +50 -0
- package/dist/types.d.ts +20 -13
- package/dist/types.js +1 -0
- package/dist/usePublicPageContext-B91dGYW1.d.ts +4367 -0
- package/dist/usePublicRouteParams-BgV6VhMi.d.ts +946 -0
- package/dist/utils.d.ts +338 -156
- package/dist/utils.js +78 -60
- package/dist/validation-g5n0hDkh.d.ts +177 -0
- package/docs/api/modules.md +1226 -1094
- package/docs/api-reference/components.md +5 -5
- package/docs/api-reference/rpc-functions.md +12 -3
- package/docs/core-concepts/rbac-system.md +8 -0
- package/docs/getting-started/cursor-rules.md +17 -20
- package/docs/getting-started/dependencies.md +1 -1
- package/docs/getting-started/setup.md +235 -0
- package/docs/implementation-guides/authentication.md +27 -0
- package/docs/implementation-guides/data-tables.md +365 -10
- package/docs/migration/ApiResult-migration.md +25 -0
- package/docs/rbac/RBAC_CONTRACT.md +0 -12
- package/docs/rbac/api-reference.md +33 -31
- package/docs/standards/0-standards-overview.md +50 -15
- package/docs/standards/1-pace-core-compliance-standards.md +62 -57
- package/docs/standards/2-project-structure-standards.md +45 -90
- package/docs/standards/3-architecture-standards.md +41 -1
- package/docs/standards/4-code-quality-standards.md +26 -6
- package/docs/standards/5-styling-standards.md +35 -1
- package/docs/standards/6-security-rbac-standards.md +288 -7
- package/docs/standards/7-api-tech-stack-standards.md +116 -17
- package/docs/standards/8-testing-documentation-standards.md +31 -0
- package/docs/standards/9-operations-standards.md +19 -0
- package/docs/standards/README.md +20 -201
- package/docs/testing/README.md +10 -0
- package/docs/testing/test-setup-for-consumers.md +916 -0
- package/docs/troubleshooting/common-issues.md +17 -1
- package/docs/troubleshooting/organisation-context-setup.md +8 -0
- package/docs/troubleshooting/print-event-name-css-variable-analysis.md +217 -0
- package/eslint-config-pace-core.cjs +24 -0
- package/package.json +14 -20
- package/scripts/build-docs.js +180 -0
- package/scripts/setup.cjs +536 -0
- package/scripts/validate.cjs +480 -0
- package/src/__mocks__/lucide-react.ts +0 -2
- package/src/__tests__/helpers/component-test-utils.test.tsx +260 -0
- package/src/__tests__/helpers/optimized-test-setup.test.ts +224 -0
- package/src/__tests__/helpers/supabaseMock.test.ts +273 -0
- package/src/__tests__/helpers/test-providers.test.tsx +99 -0
- package/src/__tests__/helpers/test-providers.tsx +37 -39
- package/src/__tests__/helpers/test-utils.test.tsx +447 -0
- package/src/__tests__/helpers/timer-utils.test.ts +371 -0
- 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.test.ts +304 -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 +379 -4
- package/src/components/AddressField/AddressField.tsx +239 -213
- 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/Button/Button.tsx +1 -1
- package/src/components/Calendar/Calendar.test.tsx +523 -131
- package/src/components/Calendar/Calendar.tsx +107 -488
- package/src/components/Card/Card.test.tsx +384 -258
- package/src/components/Card/Card.tsx +19 -10
- package/src/components/Checkbox/Checkbox.test.tsx +58 -174
- package/src/components/ContextSelector/ContextSelector.internals.tsx +204 -0
- package/src/components/ContextSelector/ContextSelector.test.tsx +360 -0
- package/src/components/ContextSelector/ContextSelector.tsx +66 -280
- package/src/components/ContextSelector/ContextSelector.types.ts +35 -0
- package/src/components/ContextSelector/useContextSelectorState.tsx +195 -0
- package/src/components/DataTable/AUDIT_REPORT.md +59 -44
- package/src/components/DataTable/DataTable.comprehensive.test.tsx +759 -0
- package/src/components/DataTable/DataTable.default-state.test.tsx +524 -0
- package/src/components/DataTable/DataTable.export.test.tsx +705 -0
- package/src/components/DataTable/DataTable.grouping-aggregation.test.tsx +658 -0
- package/src/components/DataTable/DataTable.hooks.test.tsx +192 -0
- package/src/components/DataTable/DataTable.select-label-display.test.tsx +485 -0
- package/src/components/DataTable/DataTable.test.tsx +787 -416
- package/src/components/DataTable/DataTable.tsx +14 -14
- package/src/components/DataTable/DataTableCore.integration.test.tsx +458 -0
- package/src/components/DataTable/DataTableCore.test-setup.ts +221 -0
- package/src/components/DataTable/DataTableCore.test.tsx +970 -0
- package/src/components/DataTable/README.md +155 -0
- package/src/components/DataTable/TESTING.md +101 -0
- package/src/components/DataTable/a11y.basic.test.tsx +788 -0
- package/src/components/DataTable/components/DataTableCore.tsx +126 -894
- package/src/components/DataTable/components/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/components/GroupingDropdown.tsx +2 -3
- package/src/components/DataTable/components/ImportModal.tsx +82 -408
- package/src/components/DataTable/components/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/context/DataTableContext.test.tsx +328 -0
- package/src/components/DataTable/context/DataTableContext.tsx +13 -13
- package/src/components/DataTable/core/ColumnFactory.test.ts +403 -0
- package/src/components/DataTable/core/ColumnFactory.ts +3 -3
- package/src/components/DataTable/hooks/useColumnOrderPersistence.test.ts +516 -0
- package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.test.ts +256 -0
- package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +12 -9
- package/src/components/DataTable/hooks/useDataTableConfiguration.test.ts +297 -0
- package/src/components/DataTable/hooks/useDataTableConfiguration.ts +15 -3
- package/src/components/DataTable/hooks/useDataTableDataPipeline.test.ts +270 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.test.ts +127 -0
- package/src/components/DataTable/hooks/useDataTableDeletionBatching.ts +106 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.test.ts +461 -0
- package/src/components/DataTable/hooks/useDataTableEffectiveActions.ts +238 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.test.ts +296 -0
- package/src/components/DataTable/hooks/useDataTableLayoutHandlers.ts +175 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.test.ts +203 -0
- package/src/components/DataTable/hooks/useDataTablePaginationSync.ts +109 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.test.ts +280 -0
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +81 -260
- package/src/components/DataTable/hooks/useDataTablePipeline.test.tsx +219 -0
- package/src/components/DataTable/hooks/useDataTablePipeline.tsx +239 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.test.tsx +316 -0
- package/src/components/DataTable/hooks/useDataTableRenderGuard.tsx +195 -0
- package/src/components/DataTable/hooks/useDataTableScope.test.ts +110 -0
- package/src/components/DataTable/hooks/useDataTableScope.ts +123 -0
- package/src/components/DataTable/hooks/useDataTableState.test.ts +733 -0
- package/src/components/DataTable/hooks/useDataTableState.ts +161 -114
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.test.ts +277 -0
- package/src/components/DataTable/hooks/useDataTableStateAndPersistence.ts +222 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.test.ts +93 -0
- package/src/components/DataTable/hooks/useDataTableSuperAdmin.ts +86 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.test.ts +185 -0
- package/src/components/DataTable/hooks/useDataTableTableInstance.ts +178 -0
- package/src/components/DataTable/hooks/useEffectiveColumnOrder.test.ts +183 -0
- package/src/components/DataTable/hooks/useHierarchicalState.test.ts +294 -0
- package/src/components/DataTable/hooks/useImportModalFocus.test.ts +184 -0
- package/src/components/DataTable/hooks/useImportModalFocus.ts +53 -0
- package/src/components/DataTable/hooks/useImportModalState.test.ts +390 -0
- package/src/components/DataTable/hooks/useImportModalState.ts +345 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.test.ts +787 -0
- package/src/components/DataTable/hooks/useKeyboardNavigation.ts +311 -271
- package/src/components/DataTable/hooks/usePermissionTracking.test.ts +381 -0
- package/src/components/DataTable/hooks/usePermissionTracking.ts +122 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.test.ts +258 -0
- package/src/components/DataTable/hooks/useServerSideDataEffect.ts +27 -4
- package/src/components/DataTable/hooks/useTableColumns.test.ts +499 -0
- package/src/components/DataTable/hooks/useTableColumns.ts +15 -39
- package/src/components/DataTable/hooks/useTableHandlers.test.ts +461 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +13 -22
- package/src/components/DataTable/index.ts +28 -5
- package/src/components/DataTable/keyboard.test.tsx +734 -0
- package/src/components/DataTable/mocks/MockRBACProvider.tsx +66 -0
- package/src/components/DataTable/pagination.modes.test.tsx +728 -0
- package/src/components/DataTable/ssr.strict-mode.test.tsx +319 -0
- package/src/components/DataTable/styles.test.ts +379 -0
- package/src/components/DataTable/styles.ts +0 -1
- package/src/components/DataTable/test-utils/MockDataTableComponents.tsx +55 -0
- package/src/components/DataTable/test-utils/dataFactories.ts +103 -0
- package/src/components/DataTable/test-utils/featureConfig.ts +10 -0
- package/src/components/DataTable/test-utils/sharedTestUtils.ts +419 -0
- package/src/components/DataTable/test-utils.ts +94 -0
- package/src/components/DataTable/types/actions.ts +71 -0
- package/src/components/DataTable/types/base.ts +39 -0
- package/src/components/DataTable/types/columns.ts +125 -0
- package/src/components/DataTable/types/export.ts +32 -0
- package/src/components/DataTable/types/features.ts +81 -0
- package/src/components/DataTable/types/hierarchical.ts +44 -0
- package/src/components/DataTable/types/index.ts +43 -0
- package/src/components/DataTable/types/pagination.ts +85 -0
- package/src/components/DataTable/types/performance.ts +47 -0
- package/src/components/DataTable/types/props.ts +62 -0
- package/src/components/DataTable/types/rbac.ts +45 -0
- package/src/components/DataTable/ui/layout/DataTableCore.test.tsx +1194 -0
- package/src/components/DataTable/ui/layout/DataTableCore.tsx +345 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.test.tsx +438 -0
- package/src/components/DataTable/ui/layout/DataTableErrorBoundary.tsx +225 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.test.tsx +1352 -0
- package/src/components/DataTable/ui/layout/DataTableLayout.tsx +661 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.test.tsx +91 -0
- package/src/components/DataTable/ui/modals/BulkDeleteConfirmDialog.tsx +43 -0
- package/src/components/DataTable/ui/modals/DataTableModals.test.tsx +749 -0
- package/src/components/DataTable/ui/modals/DataTableModals.tsx +341 -0
- package/src/components/DataTable/ui/modals/ImportModal.test.tsx +1834 -0
- package/src/components/DataTable/ui/modals/ImportModal.tsx +197 -0
- package/src/components/DataTable/ui/modals/ImportModalFailedRowsSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalFileSection.tsx +148 -0
- package/src/components/DataTable/ui/modals/ImportModalPreviewSection.tsx +60 -0
- package/src/components/DataTable/ui/modals/ImportModalSummarySection.tsx +59 -0
- package/src/components/DataTable/ui/modals/importModalPersistence.ts +73 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.test.tsx +245 -0
- package/src/components/DataTable/ui/shared/AccessDeniedPage.tsx +159 -0
- package/src/components/DataTable/ui/shared/ActionButtons.test.tsx +921 -0
- package/src/components/DataTable/ui/shared/ActionButtons.tsx +195 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.test.tsx +497 -0
- package/src/components/DataTable/ui/shared/ColumnFilter.tsx +113 -0
- package/src/components/DataTable/ui/shared/PaginationControls.test.tsx +451 -0
- package/src/components/DataTable/ui/shared/PaginationControls.tsx +291 -0
- package/src/components/DataTable/ui/shared/SortIndicator.test.tsx +135 -0
- package/src/components/DataTable/ui/shared/SortIndicator.tsx +50 -0
- package/src/components/DataTable/ui/table/EditFields.test.tsx +526 -0
- package/src/components/DataTable/ui/table/EditFields.tsx +355 -0
- package/src/components/DataTable/ui/table/EditableRow.test.tsx +1003 -0
- package/src/components/DataTable/ui/table/EditableRow.tsx +444 -0
- package/src/components/DataTable/ui/table/EmptyState.test.tsx +360 -0
- package/src/components/DataTable/ui/table/EmptyState.tsx +74 -0
- package/src/components/DataTable/ui/table/FilterRow.test.tsx +416 -0
- package/src/components/DataTable/ui/table/FilterRow.tsx +148 -0
- package/src/components/DataTable/ui/table/LoadingState.test.tsx +77 -0
- package/src/components/DataTable/ui/table/LoadingState.tsx +17 -0
- package/src/components/DataTable/ui/table/RowComponent.test.tsx +1024 -0
- package/src/components/DataTable/ui/table/RowComponent.tsx +429 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.test.tsx +1273 -0
- package/src/components/DataTable/ui/table/UnifiedTableBody.tsx +440 -0
- package/src/components/DataTable/ui/table/cellValueUtils.test.ts +453 -0
- package/src/components/DataTable/ui/table/cellValueUtils.ts +40 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.test.tsx +551 -0
- package/src/components/DataTable/ui/toolbar/BulkOperationsDropdown.tsx +160 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.test.tsx +751 -0
- package/src/components/DataTable/ui/toolbar/ColumnVisibilityDropdown.tsx +114 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.test.tsx +629 -0
- package/src/components/DataTable/ui/toolbar/DataTableToolbar.tsx +271 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.test.tsx +621 -0
- package/src/components/DataTable/ui/toolbar/GroupingDropdown.tsx +107 -0
- package/src/components/DataTable/utils/a11yUtils.test.ts +548 -0
- package/src/components/DataTable/utils/a11yUtils.ts +1 -1
- package/src/components/DataTable/utils/aggregationUtils.test.ts +288 -0
- package/src/components/DataTable/utils/aggregationUtils.ts +5 -5
- package/src/components/DataTable/utils/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/csvParse.test.ts +74 -0
- package/src/components/DataTable/utils/csvParse.ts +65 -0
- package/src/components/DataTable/utils/errorHandling.test.ts +209 -0
- package/src/components/DataTable/utils/errorHandling.ts +3 -1
- package/src/components/DataTable/utils/exportUtils.test.ts +954 -0
- package/src/components/DataTable/utils/exportUtils.ts +1 -1
- package/src/components/DataTable/utils/flexibleImport.test.ts +573 -0
- package/src/components/DataTable/utils/flexibleImport.ts +3 -186
- package/src/components/DataTable/utils/hierarchicalSorting.test.ts +235 -0
- package/src/components/DataTable/utils/hierarchicalSorting.ts +3 -3
- package/src/components/DataTable/utils/hierarchicalUtils.test.ts +586 -0
- package/src/components/DataTable/utils/importDateParser.test.ts +162 -0
- package/src/components/DataTable/utils/importDateParser.ts +114 -0
- package/src/components/DataTable/utils/importValueParser.test.ts +138 -0
- package/src/components/DataTable/utils/importValueParser.ts +91 -0
- package/src/components/DataTable/utils/paginationUtils.test.ts +593 -0
- package/src/components/DataTable/utils/paginationUtils.ts +7 -4
- package/src/components/DataTable/utils/performanceUtils.test.ts +470 -0
- package/src/components/DataTable/utils/performanceUtils.ts +1 -1
- package/src/components/DataTable/utils/rowUtils.test.ts +235 -0
- package/src/components/DataTable/utils/selectFieldUtils.test.ts +271 -0
- package/src/components/DataTable/utils/selectFieldUtils.ts +97 -67
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +18 -25
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +1 -1
- package/src/components/DateTimeField/DateTimeField.test.tsx +3 -16
- package/src/components/DateTimeField/DateTimeField.tsx +1 -1
- package/src/components/Dialog/Dialog.test-utils.ts +49 -0
- package/src/components/Dialog/Dialog.test.tsx +2865 -458
- package/src/components/Dialog/Dialog.tsx +183 -986
- package/src/components/Dialog/dialogLock.test.ts +238 -0
- package/src/components/Dialog/dialogLock.ts +98 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Dialog/useDialogDimensions.test.ts +163 -0
- package/src/components/Dialog/useDialogDimensions.ts +140 -0
- package/src/components/Dialog/useDialogLifecycle.test.ts +358 -0
- package/src/components/Dialog/useDialogLifecycle.ts +135 -0
- package/src/components/Dialog/useDialogPersistence.test.ts +381 -0
- package/src/components/Dialog/useDialogPersistence.ts +357 -0
- 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 +479 -247
- package/src/components/FileDisplay/FileDisplay.tsx +29 -659
- package/src/components/FileDisplay/FileDisplayContent.test.tsx +395 -0
- package/src/components/FileDisplay/FileDisplayContent.tsx +242 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.test.tsx +74 -0
- package/src/components/FileDisplay/FileDisplayDeleteConfirmDialog.tsx +38 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.test.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayEmptyView.tsx +33 -0
- package/src/components/FileDisplay/FileDisplayErrorView.test.tsx +71 -0
- package/src/components/FileDisplay/FileDisplayErrorView.tsx +50 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.test.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingFallbackView.tsx +22 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.test.tsx +21 -0
- package/src/components/FileDisplay/FileDisplayLoadingView.tsx +23 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.test.tsx +101 -0
- package/src/components/FileDisplay/FileDisplayMultipleFilesView.tsx +109 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.test.tsx +58 -0
- package/src/components/FileDisplay/FileDisplaySingleDocumentLinkView.tsx +48 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.test.tsx +111 -0
- package/src/components/FileDisplay/FileDisplaySingleFileWithActionsView.tsx +270 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.test.tsx +78 -0
- package/src/components/FileDisplay/FileDisplaySingleImageView.tsx +67 -0
- package/src/components/FileDisplay/fallbackUtils.test.ts +50 -0
- package/src/components/FileDisplay/fallbackUtils.ts +44 -0
- package/src/components/FileDisplay/fetchFileDisplayData.ts +24 -0
- package/src/components/FileDisplay/fetchFileDisplayData.unit.test.ts +183 -0
- package/src/components/FileDisplay/fileDisplayUtils.test.ts +58 -0
- package/src/components/FileDisplay/fileDisplayUtils.ts +24 -0
- package/src/components/FileDisplay/index.tsx +1 -1
- package/src/components/FileDisplay/useFileDisplay.test.ts +538 -0
- package/src/components/FileDisplay/useFileDisplay.ts +515 -0
- package/src/components/FileDisplay/useFileDisplay.unit.test.ts +1438 -0
- package/src/components/FileDisplay/useFileDisplayData.ts +126 -0
- package/src/components/FileDisplay/usePublicFileDisplay.test.ts +729 -0
- package/src/components/FileDisplay/usePublicFileDisplay.ts +579 -0
- package/src/components/FileUpload/FileUpload.test.tsx +69 -27
- package/src/components/FileUpload/FileUpload.tsx +112 -527
- package/src/components/FileUpload/FileUploadDropZone.tsx +112 -0
- package/src/components/FileUpload/FileUploadProgressItem.tsx +86 -0
- package/src/components/FileUpload/FileUploadProgressList.tsx +40 -0
- package/src/components/FileUpload/index.tsx +1 -1
- package/src/components/FileUpload/useFileUploadManager.test.ts +308 -0
- package/src/components/FileUpload/useFileUploadManager.ts +454 -0
- package/src/components/FileUpload/useResolvedAppId.test.ts +102 -0
- package/src/components/FileUpload/useResolvedAppId.ts +77 -0
- package/src/components/Footer/Footer.test.tsx +15 -382
- package/src/components/Footer/Footer.tsx +8 -125
- package/src/components/Form/Form.test.tsx +425 -88
- package/src/components/Form/Form.tsx +91 -299
- package/src/components/Form/useFormPersistence.ts +257 -0
- package/src/components/Header/Header.test.tsx +653 -163
- package/src/components/Header/Header.tsx +62 -44
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +35 -76
- package/src/components/Input/Input.test.tsx +34 -120
- package/src/components/Input/Input.tsx +1 -1
- package/src/components/Label/Label.test.tsx +46 -45
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +8 -11
- package/src/components/LoginForm/LoginForm.test.tsx +0 -1
- package/src/components/NavigationMenu/HierarchicalNavItem.tsx +104 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +2422 -102
- package/src/components/NavigationMenu/NavigationMenu.tsx +62 -362
- package/src/components/NavigationMenu/index.ts +6 -1
- package/src/components/NavigationMenu/navigationPermissionHelper.ts +188 -0
- package/src/components/NavigationMenu/useNavigationFiltering.test.ts +1949 -0
- package/src/components/NavigationMenu/useNavigationFiltering.ts +199 -308
- package/src/components/NavigationMenu/useNavigationScope.ts +125 -0
- package/src/components/PaceAppLayout/PaceAppLayout.edge-cases.test.tsx +1322 -0
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +50 -49
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +81 -38
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +103 -85
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +774 -44
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +282 -764
- package/src/components/PaceAppLayout/README.md +0 -9
- package/src/components/PaceAppLayout/test-setup.tsx +15 -9
- package/src/components/PaceAppLayout/useFilteredNavItems.ts +304 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutConfig.ts +142 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutGate.tsx +150 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutPermissions.ts +162 -0
- package/src/components/PaceAppLayout/usePaceAppLayoutScope.ts +79 -0
- package/src/components/PaceAppLayout/useRoleBasedRouteAccess.ts +157 -0
- package/src/components/PaceAppLayout/useSuperAdminFallback.ts +58 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +782 -20
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +33 -125
- package/src/components/PaceLoginPage/useLoginAppAccess.ts +153 -0
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +1 -1
- package/src/components/Progress/Progress.test.tsx +127 -1
- package/src/components/Progress/Progress.tsx +1 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +1196 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +29 -217
- package/src/components/ProtectedRoute/useProtectedRouteState.ts +128 -0
- package/src/components/ProtectedRoute/useVisibilityRedirectGrace.ts +89 -0
- package/src/components/PublicLayout/PublicLayout.test.tsx +1640 -38
- package/src/components/PublicLayout/PublicPageContext.ts +28 -0
- package/src/components/PublicLayout/PublicPageLayout.tsx +134 -75
- package/src/components/PublicLayout/PublicPageProvider.tsx +7 -42
- package/src/components/PublicLayout/usePublicPageContext.ts +36 -0
- package/src/components/Select/Select.test.tsx +45 -8
- package/src/components/Select/Select.tsx +57 -40
- package/src/components/Select/context.test.tsx +56 -0
- package/src/components/Select/text.test.tsx +104 -0
- package/src/components/Select/text.ts +26 -0
- package/src/components/Select/types.ts +3 -0
- package/src/components/Select/useSelectEvents.test.ts +279 -0
- package/src/components/Select/useSelectEvents.ts +87 -0
- package/src/components/Select/useSelectSearch.test.tsx +295 -0
- package/src/components/Select/useSelectSearch.ts +91 -0
- package/src/components/Select/useSelectState.test.ts +268 -0
- package/src/components/Select/useSelectState.ts +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 +395 -317
- package/src/components/Tabs/Tabs.test.tsx +270 -0
- package/src/components/Tabs/Tabs.tsx +4 -4
- package/src/components/Textarea/Textarea.test.tsx +11 -38
- package/src/components/Toast/Toast.test.tsx +425 -496
- package/src/components/Tooltip/Tooltip.test.tsx +4 -21
- package/src/components/UserMenu/UserMenu.test.tsx +1 -21
- package/src/components/UserMenu/UserMenu.tsx +0 -1
- package/src/components/index.test.ts +346 -0
- package/src/components/index.ts +12 -1
- package/src/constants/performance.test.ts +91 -0
- package/src/hooks/ServiceHooks.test.tsx +725 -0
- package/src/hooks/hooks.integration.test.tsx +608 -0
- package/src/hooks/index.ts +18 -3
- package/src/hooks/index.unit.test.ts +220 -0
- 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/usePublicRouteParams.test.ts +595 -0
- package/src/hooks/public/usePublicRouteParams.ts +2 -2
- package/src/hooks/services/useAuth.ts +9 -7
- 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 +170 -47
- package/src/hooks/useAddressAutocomplete.ts +109 -81
- package/src/hooks/useApiFetch.unit.test.ts +111 -0
- package/src/hooks/useAppConfig.ts +13 -3
- package/src/hooks/useAppConfig.unit.test.ts +712 -0
- package/src/hooks/useComponentPerformance.unit.test.tsx +314 -0
- package/src/hooks/useDataTablePerformance.ts +111 -130
- package/src/hooks/useDataTablePerformance.unit.test.ts +720 -0
- package/src/hooks/useDataTableState.test.ts +170 -0
- package/src/hooks/useDataTableState.ts +5 -5
- package/src/hooks/useDebounce.unit.test.ts +157 -0
- package/src/hooks/useEventTheme.test.ts +70 -18
- package/src/hooks/useEventTheme.ts +50 -22
- package/src/hooks/useEvents.ts +49 -2
- package/src/hooks/useEvents.unit.test.ts +227 -0
- package/src/hooks/useFileReference.test.ts +388 -107
- package/src/hooks/useFileReference.ts +184 -179
- package/src/hooks/useFileUrl.ts +1 -1
- package/src/hooks/useFileUrl.unit.test.ts +686 -0
- package/src/hooks/useFileUrlCache.test.ts +319 -0
- package/src/hooks/useFileUrlCache.ts +5 -2
- package/src/hooks/useFocusManagement.unit.test.ts +604 -0
- package/src/hooks/useFocusTrap.unit.test.tsx +613 -0
- package/src/hooks/useFormDialog.test.ts +307 -0
- package/src/hooks/useFormDialog.ts +2 -2
- package/src/hooks/useInactivityTracker.ts +141 -134
- package/src/hooks/useInactivityTracker.unit.test.ts +446 -0
- package/src/hooks/useIsMobile.unit.test.ts +317 -0
- package/src/hooks/useIsPrint.ts +62 -0
- package/src/hooks/useIsPrint.unit.test.ts +545 -0
- package/src/hooks/useKeyboardShortcuts.unit.test.ts +907 -0
- package/src/hooks/useOrganisationPermissions.test.ts +1 -2
- package/src/hooks/useOrganisationPermissions.ts +1 -4
- package/src/hooks/useOrganisationPermissions.unit.test.tsx +293 -0
- package/src/hooks/useOrganisationSecurity.test.ts +4 -33
- package/src/hooks/useOrganisationSecurity.ts +192 -203
- package/src/hooks/useOrganisationSecurity.unit.test.tsx +959 -0
- package/src/hooks/useOrganisations.ts +1 -1
- package/src/hooks/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/usePerformanceMonitor.ts +1 -1
- package/src/hooks/usePerformanceMonitor.unit.test.ts +693 -0
- package/src/hooks/usePermissionCache.test.ts +298 -329
- package/src/hooks/usePermissionCache.ts +277 -276
- package/src/hooks/usePreventTabReload.test.ts +307 -0
- package/src/hooks/usePublicEvent.simple.test.ts +794 -0
- package/src/hooks/usePublicEvent.test.ts +670 -0
- package/src/hooks/usePublicEvent.unit.test.ts +638 -0
- package/src/hooks/usePublicFileDisplay.test.ts +948 -0
- package/src/hooks/usePublicRouteParams.unit.test.ts +442 -0
- package/src/hooks/useQueryCache.test.ts +391 -0
- package/src/hooks/useQueryCache.ts +7 -9
- package/src/hooks/useRBAC.unit.test.ts +253 -0
- package/src/hooks/useSessionDraft.test.ts +556 -0
- package/src/hooks/useSessionDraft.ts +14 -11
- package/src/hooks/useSessionRestoration.ts +1 -1
- package/src/hooks/useSessionRestoration.unit.test.tsx +381 -0
- package/src/hooks/useStorage.ts +94 -54
- package/src/hooks/useStorage.unit.test.ts +684 -0
- package/src/hooks/useToast.test.ts +413 -0
- package/src/hooks/useToast.ts +2 -2
- package/src/hooks/useToast.unit.test.tsx +481 -0
- package/src/hooks/useZodForm.ts +3 -3
- package/src/hooks/useZodForm.unit.test.tsx +191 -0
- package/src/icons/index.test.ts +133 -0
- package/src/icons/index.ts +3 -1
- package/src/index.test.ts +528 -0
- package/src/index.ts +56 -9
- package/src/providers/AuthProvider.test.tsx +218 -0
- package/src/providers/EventProvider.test.tsx +487 -0
- package/src/providers/InactivityProvider.test-helper.tsx +40 -0
- package/src/providers/InactivityProvider.test.tsx +421 -0
- package/src/providers/ProviderLifecycle.test.tsx +308 -0
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +7 -12
- package/src/providers/UnifiedAuthProvider.test.tsx +503 -0
- package/src/providers/index.test.ts +138 -0
- package/src/providers/services/AuthServiceContext.ts +27 -0
- package/src/providers/services/AuthServiceProvider.integration.test.tsx +229 -0
- package/src/providers/services/AuthServiceProvider.test.tsx +638 -0
- package/src/providers/services/AuthServiceProvider.tsx +81 -20
- package/src/providers/services/EventServiceContext.ts +25 -0
- package/src/providers/services/EventServiceProvider.test.tsx +839 -0
- package/src/providers/services/EventServiceProvider.tsx +11 -20
- package/src/providers/services/InactivityServiceContext.ts +25 -0
- package/src/providers/services/InactivityServiceProvider.test.tsx +662 -0
- package/src/providers/services/InactivityServiceProvider.tsx +7 -17
- package/src/providers/services/OrganisationServiceContext.ts +25 -0
- package/src/providers/services/OrganisationServiceProvider.test.tsx +440 -0
- package/src/providers/services/OrganisationServiceProvider.tsx +7 -17
- package/src/providers/services/UnifiedAuthContext.ts +102 -0
- package/src/providers/services/UnifiedAuthProvider.advanced.test.tsx +434 -0
- package/src/providers/services/UnifiedAuthProvider.appId.test.tsx +408 -0
- package/src/providers/services/UnifiedAuthProvider.integration.test.tsx +304 -0
- package/src/providers/services/UnifiedAuthProvider.test.tsx +212 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +147 -497
- package/src/providers/services/contexts.test.tsx +281 -0
- package/src/providers/services/useUnifiedAuth.test.tsx +251 -0
- package/src/providers/services/useUnifiedAuth.ts +29 -0
- package/src/providers/services/useUnifiedAuthContextValue.ts +279 -0
- package/src/providers/useInactivity.test-helper.ts +27 -0
- package/src/rbac/README.md +5 -5
- package/src/rbac/adapters.comprehensive.test.tsx +429 -0
- package/src/rbac/adapters.test.tsx +654 -0
- package/src/rbac/adapters.tsx +53 -38
- package/src/rbac/api.test.ts +986 -259
- package/src/rbac/api.ts +260 -216
- package/src/rbac/audit-batched.test.ts +550 -0
- package/src/rbac/audit-batched.ts +5 -4
- package/src/rbac/audit.test.ts +225 -28
- package/src/rbac/audit.ts +26 -18
- package/src/rbac/auth-rbac-security.integration.test.tsx +300 -0
- package/src/rbac/auth-rbac.e2e.test.tsx +510 -0
- package/src/rbac/cache-invalidation.test.ts +715 -0
- 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.test.tsx +324 -0
- package/src/rbac/components/AccessDenied.tsx +20 -18
- package/src/rbac/components/NavigationGuard.test.tsx +1148 -0
- package/src/rbac/components/NavigationGuard.tsx +10 -8
- package/src/rbac/components/PagePermissionGuard.guard.test.tsx +236 -0
- package/src/rbac/components/PagePermissionGuard.performance.test.tsx +252 -0
- package/src/rbac/components/PagePermissionGuard.race-condition.test.tsx +243 -0
- package/src/rbac/components/PagePermissionGuard.test.tsx +1430 -0
- package/src/rbac/components/PagePermissionGuard.tsx +188 -381
- package/src/rbac/components/PagePermissionGuard.verification.test.tsx +185 -0
- package/src/rbac/config.test.ts +131 -48
- package/src/rbac/config.ts +69 -26
- package/src/rbac/docs/event-based-apps.md +26 -13
- package/src/rbac/engine.comprehensive.test.ts +808 -0
- package/src/rbac/engine.test.ts +974 -130
- package/src/rbac/engine.ts +53 -13
- package/src/rbac/errors.test.ts +99 -87
- package/src/rbac/errors.ts +89 -55
- package/src/rbac/eslint-rules.js +2 -2
- package/src/rbac/hooks/permissions/runPermissionCheck.ts +77 -0
- package/src/rbac/hooks/permissions/useAccessLevel.test.ts +622 -0
- package/src/rbac/hooks/permissions/useAccessLevel.ts +23 -14
- package/src/rbac/hooks/permissions/useCan.test.ts +798 -0
- package/src/rbac/hooks/permissions/useCan.ts +173 -253
- package/src/rbac/hooks/permissions/useMultiplePermissions.test.ts +843 -0
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +63 -10
- package/src/rbac/hooks/permissions/usePermissions.test.ts +543 -0
- package/src/rbac/hooks/permissions/usePermissions.ts +50 -78
- package/src/rbac/hooks/useCan.test.ts +348 -32
- package/src/rbac/hooks/usePageAccessLogging.ts +160 -0
- package/src/rbac/hooks/usePageGuardScope.ts +117 -0
- package/src/rbac/hooks/usePagePermissionCheck.ts +67 -0
- package/src/rbac/hooks/usePermissions.integration.test.ts +427 -0
- package/src/rbac/hooks/usePermissions.stability.test.ts +268 -0
- package/src/rbac/hooks/usePermissions.test.ts +459 -33
- package/src/rbac/hooks/usePermissions.ts +5 -7
- package/src/rbac/hooks/useRBAC.test.ts +1784 -21
- package/src/rbac/hooks/useRBAC.ts +148 -88
- package/src/rbac/hooks/useResolvedScope.test.ts +442 -5
- package/src/rbac/hooks/useResolvedScope.ts +4 -1
- package/src/rbac/hooks/useResourcePermissions.test.ts +561 -24
- package/src/rbac/hooks/useResourcePermissions.ts +76 -140
- package/src/rbac/hooks/useResourcePermissionsSuperAdmin.ts +67 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +634 -61
- package/src/rbac/hooks/useRoleManagement.ts +158 -586
- package/src/rbac/hooks/useSecureSupabase.test.ts +1179 -0
- package/src/rbac/hooks/useSecureSupabase.ts +21 -14
- package/src/rbac/hooks/useSuperAdminCheck.ts +80 -0
- package/src/rbac/index.test.ts +107 -0
- package/src/rbac/index.ts +32 -32
- package/src/rbac/performance.test.ts +451 -0
- package/src/rbac/permissions.test.ts +149 -68
- package/src/rbac/permissions.ts +0 -3
- package/src/rbac/rbac-core.test.tsx +276 -0
- package/src/rbac/rbac-engine-core-logic.test.ts +387 -0
- package/src/rbac/rbac-engine-simplified.test.ts +252 -0
- package/src/rbac/rbac-functions.test.ts +703 -0
- package/src/rbac/rbac-integration.test.ts +523 -0
- package/src/rbac/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/request-deduplication.test.ts +352 -0
- package/src/rbac/request-deduplication.ts +5 -4
- package/src/rbac/scenarios.user-role.test.tsx +271 -0
- package/src/rbac/secureClient.test.ts +499 -115
- package/src/rbac/secureClient.ts +54 -28
- package/src/rbac/security.test.ts +448 -44
- package/src/rbac/security.ts +7 -6
- package/src/rbac/types/roleManagement.ts +66 -0
- package/src/rbac/types.test.ts +236 -0
- package/src/rbac/types.ts +7 -5
- package/src/rbac/utils/clientSecurity.test.ts +192 -0
- package/src/rbac/utils/clientSecurity.ts +6 -4
- package/src/rbac/utils/contextValidator.test.ts +126 -0
- package/src/rbac/utils/contextValidator.ts +6 -3
- package/src/rbac/utils/deep-equal.test.ts +76 -0
- package/src/rbac/utils/eventContext.test.ts +401 -0
- package/src/rbac/utils/eventContext.ts +38 -34
- package/src/rbac/utils/fetchPermissionMap.ts +13 -0
- package/src/rbac/utils/permissionMapHelpers.ts +34 -0
- package/src/rbac/utils/roleManagementRpc.ts +303 -0
- package/src/services/AuthService.edge-cases.test.ts +746 -0
- package/src/services/AuthService.restoreSession.test.ts +59 -0
- package/src/services/AuthService.test.ts +1362 -0
- package/src/services/AuthService.ts +197 -216
- package/src/services/BaseService.edge-cases.test.ts +506 -0
- package/src/services/BaseService.test.ts +363 -0
- package/src/services/EventService.edge-cases.test.ts +636 -0
- package/src/services/EventService.eventColours.test.ts +64 -0
- package/src/services/EventService.test.ts +1250 -0
- package/src/services/EventService.ts +244 -315
- package/src/services/InactivityService.edge-cases.test.ts +492 -0
- package/src/services/InactivityService.lifecycle.test.ts +406 -0
- package/src/services/InactivityService.test.ts +829 -0
- package/src/services/InactivityService.ts +172 -213
- package/src/services/OrganisationService.edge-cases.test.ts +633 -0
- package/src/services/OrganisationService.pagination.test.ts +409 -0
- package/src/services/OrganisationService.test.ts +1579 -0
- package/src/services/OrganisationService.ts +186 -257
- package/src/services/base/BaseService.test.ts +214 -0
- package/src/services/interfaces/IAuthService.test.ts +184 -0
- package/src/services/interfaces/IAuthService.ts +10 -9
- package/src/services/interfaces/IEventService.test.ts +176 -0
- package/src/services/interfaces/IInactivityService.test.ts +183 -0
- package/src/services/interfaces/IOrganisationService.test.ts +207 -0
- package/src/services/interfaces/IOrganisationService.ts +0 -1
- package/src/styles/core.css +244 -12
- package/src/theming/parseEventColours.test.ts +321 -0
- package/src/theming/parseEventColours.ts +18 -9
- package/src/theming/runtime.test.ts +495 -0
- package/src/theming/runtime.ts +72 -7
- package/src/types/api-result.ts +53 -0
- package/src/types/auth.ts +0 -1
- package/src/types/core.test.ts +397 -0
- package/src/types/database-generated.test.ts +78 -0
- package/src/types/database.generated.ts +45 -10
- package/src/types/event.ts +39 -19
- package/src/types/file-reference.test.ts +351 -0
- package/src/types/file-reference.ts +37 -12
- package/src/types/guards.test.ts +246 -0
- package/src/types/index.test.ts +265 -0
- package/src/types/index.ts +3 -0
- package/src/types/organisation.roles.test.ts +55 -0
- package/src/types/organisation.test.ts +1105 -0
- package/src/types/organisation.ts +15 -15
- package/src/types/rpc-responses.ts +33 -0
- package/src/types/supabase.ts +14 -6
- package/src/types/theme.test.ts +830 -0
- package/src/types/type-validation.test.ts +526 -0
- package/src/types/validation.test.ts +729 -0
- package/src/types/vitest-globals.d.ts +1 -1
- package/src/utils/app/appConfig.test.ts +235 -0
- package/src/utils/app/appIdResolver.test.ts +252 -57
- package/src/utils/app/appIdResolver.ts +31 -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/appConfig.unit.test.ts +55 -0
- package/src/utils/audit/audit.test.ts +354 -39
- package/src/utils/audit.unit.test.ts +69 -0
- package/src/utils/auth-utils.unit.test.ts +69 -0
- package/src/utils/bundleAnalysis.unit.test.ts +326 -0
- package/src/utils/cn.unit.test.ts +34 -0
- package/src/utils/context/organisationContext.test.ts +115 -95
- package/src/utils/context/organisationContext.ts +32 -43
- package/src/utils/context/sessionTracking.test.ts +354 -0
- package/src/utils/core/cn.test.ts +66 -0
- package/src/utils/core/debugLogger.test.ts +113 -0
- package/src/utils/core/debugLogger.ts +15 -8
- package/src/utils/core/logger.test.ts +217 -0
- package/src/utils/core/logger.ts +20 -16
- package/src/utils/core/mergeRefs.ts +24 -0
- package/src/utils/debugLogger.test.ts +417 -0
- package/src/utils/device/deviceFingerprint.test.ts +8 -5
- package/src/utils/device/deviceFingerprint.ts +3 -3
- package/src/utils/deviceFingerprint.unit.test.ts +818 -0
- package/src/utils/dynamic/createLazyComponent.tsx +46 -0
- package/src/utils/dynamic/dynamicUtils.test.ts +185 -0
- package/src/utils/dynamic/dynamicUtils.ts +6 -6
- package/src/utils/dynamic/lazyLoad.test.tsx +156 -0
- package/src/utils/dynamic/lazyLoad.tsx +8 -36
- package/src/utils/dynamic/papaparseLoader.ts +7 -0
- package/src/utils/dynamicUtils.unit.test.ts +331 -0
- package/src/utils/file-reference/file-reference.test.ts +1238 -0
- package/src/utils/file-reference/index.ts +330 -348
- package/src/utils/formatDate.unit.test.ts +109 -0
- 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 +41 -85
- 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/formatting.unit.test.ts +99 -0
- package/src/utils/google-places/googlePlacesUtils.test.ts +127 -36
- package/src/utils/google-places/googlePlacesUtils.ts +67 -86
- package/src/utils/google-places/loadGoogleMapsScript.test.ts +68 -8
- package/src/utils/google-places/loadGoogleMapsScript.ts +140 -118
- package/src/utils/index.ts +52 -11
- package/src/utils/index.unit.test.ts +251 -0
- package/src/utils/lazyLoad.unit.test.tsx +319 -0
- package/src/utils/location/location.test.ts +19 -116
- package/src/utils/logger.unit.test.ts +398 -0
- package/src/utils/organisationContext.unit.test.ts +180 -0
- package/src/utils/performance/bundleAnalysis.test.ts +148 -0
- package/src/utils/performance/bundleAnalysis.ts +16 -22
- package/src/utils/performance/performanceBenchmark.test.ts +251 -0
- package/src/utils/performance/performanceBenchmark.ts +12 -4
- package/src/utils/performance/performanceBudgets.test.ts +241 -0
- package/src/utils/performance/performanceBudgets.ts +9 -6
- package/src/utils/performanceBenchmark.test.ts +174 -0
- package/src/utils/performanceBudgets.unit.test.ts +288 -0
- package/src/utils/permissionTypes.unit.test.ts +250 -0
- package/src/utils/permissionUtils.unit.test.ts +362 -0
- package/src/utils/permissions/permissionTypes.test.ts +149 -0
- package/src/utils/permissions/permissionUtils.test.ts +20 -42
- package/src/utils/persistence/keyDerivation.test.ts +306 -0
- package/src/utils/persistence/sensitiveFieldDetection.test.ts +271 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +2 -2
- package/src/utils/request-deduplication.test.ts +349 -0
- package/src/utils/request-deduplication.ts +6 -4
- package/src/utils/sanitization.unit.test.ts +346 -0
- package/src/utils/schemaUtils.unit.test.ts +441 -0
- package/src/utils/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/secureErrors.unit.test.ts +390 -0
- package/src/utils/secureStorage.unit.test.ts +289 -0
- package/src/utils/security/auth-utils.ts +38 -27
- package/src/utils/security/secureDataAccess.test.ts +22 -191
- package/src/utils/security/secureDataAccess.ts +241 -281
- 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 +212 -0
- package/src/utils/security/security.ts +15 -18
- package/src/utils/security/securityMonitor.test.ts +90 -0
- package/src/utils/security/securityMonitor.ts +1 -1
- package/src/utils/security.unit.test.ts +155 -0
- package/src/utils/securityMonitor.unit.test.ts +276 -0
- package/src/utils/sessionTracking.unit.test.ts +218 -0
- package/src/utils/storage/config.unit.test.ts +239 -0
- package/src/utils/storage/helpers.test.ts +769 -456
- package/src/utils/storage/helpers.ts +174 -253
- package/src/utils/storage/index.unit.test.ts +68 -0
- package/src/utils/storage/storageUtils.ts +32 -0
- package/src/utils/storage/types.ts +9 -2
- package/src/utils/supabase/createBaseClient.test.ts +201 -0
- package/src/utils/supabase/createBaseClient.ts +2 -1
- package/src/utils/timezone/timezone.test.ts +26 -44
- package/src/utils/timezone.test.ts +345 -0
- package/src/utils/validation/common.test.ts +115 -0
- package/src/utils/validation/csrf.test.ts +198 -0
- package/src/utils/validation/csrf.ts +42 -41
- package/src/utils/validation/htmlSanitization.ts +27 -31
- package/src/utils/validation/htmlSanitization.unit.test.ts +618 -0
- package/src/utils/validation/passwordSchema.test.ts +164 -0
- package/src/utils/validation/schema.test.ts +127 -0
- package/src/utils/validation/schema.ts +6 -3
- package/src/utils/validation/sqlInjectionProtection.test.ts +165 -0
- package/src/utils/validation/sqlInjectionProtection.ts +2 -2
- package/src/utils/validation/user.test.ts +173 -0
- package/src/utils/validation/validation.test.ts +197 -0
- package/src/utils/validation/validationUtils.test.ts +294 -0
- package/src/utils/validation.unit.test.ts +307 -0
- package/src/utils/validationUtils.unit.test.ts +558 -0
- package/src/vite-env.d.ts +6 -0
- package/dist/AuthService-DmfO5rGS.d.ts +0 -524
- package/dist/DataTable-DRUIgtUH.d.ts +0 -166
- package/dist/DataTable-SOAFXIWY.js +0 -15
- package/dist/PublicPageProvider-CIGSujI2.d.ts +0 -4147
- package/dist/UnifiedAuthProvider-7SNDOWYD.js +0 -7
- package/dist/UnifiedAuthProvider-CKvHP1MK.d.ts +0 -139
- package/dist/api-7P7DI652.js +0 -4
- package/dist/audit-MYQXYZFU.js +0 -3
- package/dist/auth-BZOJqrdd.d.ts +0 -49
- package/dist/chunk-4DDCYDQ3.js +0 -544
- package/dist/chunk-5HNSDQWH.js +0 -5046
- package/dist/chunk-5W2A3DRC.js +0 -164
- package/dist/chunk-6GLLNA6U.js +0 -31
- package/dist/chunk-7ILTDCL2.js +0 -80
- package/dist/chunk-A3W6LW53.js +0 -70
- package/dist/chunk-AHU7G2R5.js +0 -423
- package/dist/chunk-C7ZQ5O4C.js +0 -481
- package/dist/chunk-EF2UGZWY.js +0 -611
- package/dist/chunk-FEJLJNWA.js +0 -181
- package/dist/chunk-FYHN4DD5.js +0 -415
- package/dist/chunk-GS5672WG.js +0 -2003
- package/dist/chunk-HF6O3O37.js +0 -187
- package/dist/chunk-J2U36LHD.js +0 -8517
- package/dist/chunk-LX6U42O3.js +0 -2177
- package/dist/chunk-MPBLMWVR.js +0 -2161
- package/dist/chunk-OJ4SKRSV.js +0 -105
- package/dist/chunk-S6ZQKDY6.js +0 -62
- package/dist/chunk-S7DKJPLT.js +0 -699
- package/dist/chunk-T5CVK4R3.js +0 -2816
- package/dist/chunk-TTRFSOKR.js +0 -121
- package/dist/chunk-Z2FNRKF3.js +0 -994
- package/dist/database.generated-DT8JTZiP.d.ts +0 -9406
- package/dist/event-CW5YB_2p.d.ts +0 -239
- package/dist/file-reference-BavO2eQj.d.ts +0 -148
- package/dist/functions-lBy5L2ry.d.ts +0 -208
- package/dist/timezone-0AyangqX.d.ts +0 -697
- package/dist/types-BeoeWV5I.d.ts +0 -110
- package/dist/types-DXstZpNI.d.ts +0 -614
- package/dist/types-t9H8qKRw.d.ts +0 -55
- package/dist/usePublicRouteParams-DQLrDqDb.d.ts +0 -876
- package/dist/useToast-AyaT-x7p.d.ts +0 -68
- package/dist/validation-643vUDZW.d.ts +0 -177
- package/scripts/build-docs-incremental.js +0 -179
- package/scripts/eslint-audit.cjs +0 -123
- package/scripts/generate-docs.js +0 -157
- package/scripts/install-cursor-rules.cjs +0 -255
- package/scripts/install-eslint-config.cjs +0 -349
- package/scripts/setup-build-cache.js +0 -73
- package/scripts/validate-pre-publish.js +0 -145
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +0 -260
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +0 -224
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +0 -273
- package/src/__tests__/helpers/__tests__/test-providers.test.tsx +0 -99
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +0 -448
- package/src/__tests__/helpers/__tests__/timer-utils.test.ts +0 -371
- package/src/__tests__/hooks/usePermissions.test.ts +0 -268
- package/src/__tests__/integration/UserProfile.test.tsx +0 -124
- package/src/__tests__/public-recipe-view.test.ts +0 -228
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +0 -220
- package/src/__tests__/rls-policies.test.ts +0 -471
- package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +0 -759
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +0 -524
- package/src/components/DataTable/__tests__/DataTable.export.test.tsx +0 -705
- package/src/components/DataTable/__tests__/DataTable.grouping-aggregation.test.tsx +0 -658
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +0 -192
- package/src/components/DataTable/__tests__/DataTable.select-label-display.test.tsx +0 -483
- package/src/components/DataTable/__tests__/DataTable.test.tsx +0 -876
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +0 -220
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +0 -1474
- package/src/components/DataTable/__tests__/README.md +0 -145
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +0 -788
- package/src/components/DataTable/__tests__/keyboard.test.tsx +0 -756
- package/src/components/DataTable/__tests__/mocks/MockRBACProvider.tsx +0 -66
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +0 -730
- package/src/components/DataTable/__tests__/ssr.strict-mode.test.tsx +0 -325
- package/src/components/DataTable/__tests__/styles.test.ts +0 -382
- package/src/components/DataTable/__tests__/test-utils/dataFactories.ts +0 -103
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +0 -380
- package/src/components/DataTable/__tests__/test-utils.ts +0 -94
- package/src/components/DataTable/components/AccessDeniedPage.tsx +0 -159
- package/src/components/DataTable/components/ActionButtons.tsx +0 -190
- package/src/components/DataTable/components/BulkOperationsDropdown.tsx +0 -160
- package/src/components/DataTable/components/ColumnFilter.tsx +0 -118
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +0 -114
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +0 -225
- package/src/components/DataTable/components/DataTableLayout.tsx +0 -573
- package/src/components/DataTable/components/DataTableModals.tsx +0 -245
- package/src/components/DataTable/components/DataTableToolbar.tsx +0 -271
- package/src/components/DataTable/components/EditFields.tsx +0 -327
- package/src/components/DataTable/components/EditableRow.tsx +0 -462
- package/src/components/DataTable/components/EmptyState.tsx +0 -79
- package/src/components/DataTable/components/FilterRow.tsx +0 -141
- package/src/components/DataTable/components/LoadingState.tsx +0 -17
- package/src/components/DataTable/components/PaginationControls.tsx +0 -289
- package/src/components/DataTable/components/RowComponent.tsx +0 -403
- package/src/components/DataTable/components/SortIndicator.tsx +0 -50
- package/src/components/DataTable/components/UnifiedTableBody.tsx +0 -355
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +0 -657
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +0 -913
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +0 -572
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +0 -612
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +0 -708
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +0 -479
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +0 -475
- package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +0 -157
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +0 -1061
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +0 -437
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +0 -474
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +0 -617
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +0 -1093
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +0 -139
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +0 -519
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +0 -1004
- package/src/components/DataTable/components/cellValueUtils.ts +0 -40
- package/src/components/DataTable/components/hooks/useImportModalFocus.ts +0 -53
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -122
- package/src/components/DataTable/components/index.ts +0 -16
- package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +0 -342
- package/src/components/DataTable/core/ActionManager.ts +0 -235
- package/src/components/DataTable/core/ColumnManager.ts +0 -205
- package/src/components/DataTable/core/DataManager.ts +0 -188
- package/src/components/DataTable/core/LocalDataAdapter.ts +0 -274
- package/src/components/DataTable/core/PluginRegistry.ts +0 -229
- package/src/components/DataTable/core/StateManager.ts +0 -312
- package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -123
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +0 -305
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -84
- package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -115
- package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +0 -100
- package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +0 -120
- package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -104
- package/src/components/DataTable/core/index.ts +0 -1
- package/src/components/DataTable/core/interfaces.ts +0 -338
- package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +0 -521
- package/src/components/DataTable/hooks/__tests__/useColumnVisibilityPersistence.test.ts +0 -167
- package/src/components/DataTable/hooks/__tests__/useDataTableConfiguration.test.ts +0 -124
- package/src/components/DataTable/hooks/__tests__/useDataTableDataPipeline.test.ts +0 -117
- package/src/components/DataTable/hooks/__tests__/useDataTablePermissions.test.ts +0 -102
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +0 -596
- package/src/components/DataTable/hooks/__tests__/useEffectiveColumnOrder.test.ts +0 -53
- package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +0 -214
- package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +0 -448
- package/src/components/DataTable/hooks/index.ts +0 -13
- package/src/components/DataTable/types.ts +0 -761
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +0 -612
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +0 -94
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +0 -266
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +0 -954
- package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +0 -573
- package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +0 -247
- package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +0 -570
- package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +0 -470
- package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +0 -251
- package/src/components/DataTable/utils/__tests__/selectFieldUtils.test.ts +0 -207
- package/src/components/DataTable/utils/index.ts +0 -10
- package/src/components/PublicLayout/index.ts +0 -32
- package/src/components/Select/hooks/useSelectEvents.ts +0 -87
- package/src/components/Select/hooks/useSelectSearch.ts +0 -91
- package/src/components/Select/hooks/useSelectState.ts +0 -104
- package/src/components/Select/utils/text.ts +0 -26
- package/src/hooks/__tests__/ServiceHooks.test.tsx +0 -615
- package/src/hooks/__tests__/hooks.integration.test.tsx +0 -607
- package/src/hooks/__tests__/index.unit.test.ts +0 -220
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +0 -111
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +0 -347
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +0 -144
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +0 -776
- package/src/hooks/__tests__/useDataTableState.test.ts +0 -76
- package/src/hooks/__tests__/useDebounce.unit.test.ts +0 -82
- package/src/hooks/__tests__/useEvents.unit.test.ts +0 -252
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +0 -1112
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +0 -916
- package/src/hooks/__tests__/useFileUrlCache.test.ts +0 -129
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +0 -230
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +0 -828
- package/src/hooks/__tests__/useFormDialog.test.ts +0 -478
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +0 -446
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +0 -317
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +0 -910
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +0 -294
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +0 -961
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +0 -369
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +0 -694
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
- package/src/hooks/__tests__/usePreventTabReload.test.ts +0 -88
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -785
- package/src/hooks/__tests__/usePublicEvent.test.ts +0 -678
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +0 -630
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +0 -951
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +0 -443
- package/src/hooks/__tests__/useQueryCache.test.ts +0 -144
- package/src/hooks/__tests__/useRBAC.unit.test.ts +0 -236
- package/src/hooks/__tests__/useSessionDraft.test.ts +0 -163
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +0 -390
- package/src/hooks/__tests__/useStorage.unit.test.ts +0 -751
- package/src/hooks/__tests__/useToast.unit.test.tsx +0 -481
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +0 -37
- package/src/hooks/public/index.ts +0 -36
- package/src/hooks/public/usePublicFileDisplay.ts +0 -504
- package/src/hooks/useFileDisplay.ts +0 -715
- package/src/providers/OrganisationProvider.tsx +0 -92
- package/src/providers/__tests__/AuthProvider.test.tsx +0 -287
- package/src/providers/__tests__/EventProvider.test.tsx +0 -551
- package/src/providers/__tests__/InactivityProvider.test-helper.tsx +0 -65
- package/src/providers/__tests__/InactivityProvider.test.tsx +0 -572
- package/src/providers/__tests__/OrganisationProvider.test.tsx +0 -617
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +0 -424
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +0 -596
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +0 -263
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +0 -294
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +0 -434
- package/src/rbac/__tests__/auth-rbac-security.integration.test.tsx +0 -313
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +0 -486
- package/src/rbac/__tests__/cache-invalidation.test.ts +0 -399
- package/src/rbac/__tests__/engine.comprehensive.test.ts +0 -813
- package/src/rbac/__tests__/isSuperAdmin.real.test.ts +0 -82
- package/src/rbac/__tests__/rbac-core.test.tsx +0 -276
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +0 -392
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +0 -258
- package/src/rbac/__tests__/rbac-functions.test.ts +0 -647
- package/src/rbac/__tests__/rbac-integration.test.ts +0 -524
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +0 -456
- package/src/rbac/__tests__/scenarios.user-role.test.tsx +0 -282
- 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/__tests__/NavigationGuard.test.tsx +0 -975
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +0 -248
- package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +0 -242
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +0 -1107
- package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +0 -184
- package/src/rbac/components/index.ts +0 -26
- package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +0 -432
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +0 -579
- 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__/contextValidator.test.ts +0 -128
- package/src/rbac/utils/__tests__/deep-equal.test.ts +0 -53
- package/src/rbac/utils/__tests__/eventContext.test.ts +0 -433
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -490
- package/src/services/__tests__/AuthService.restoreSession.test.ts +0 -39
- package/src/services/__tests__/AuthService.test.ts +0 -1332
- package/src/services/__tests__/BaseService.test.ts +0 -314
- package/src/services/__tests__/EventService.eventColours.test.ts +0 -76
- package/src/services/__tests__/EventService.test.ts +0 -1025
- package/src/services/__tests__/InactivityService.lifecycle.test.ts +0 -411
- package/src/services/__tests__/InactivityService.test.ts +0 -654
- package/src/services/__tests__/OrganisationService.pagination.test.ts +0 -409
- package/src/services/__tests__/OrganisationService.test.ts +0 -1176
- package/src/theming/__tests__/parseEventColours.test.ts +0 -321
- package/src/theming/__tests__/runtime.test.ts +0 -569
- package/src/types/__tests__/file-reference.test.ts +0 -447
- package/src/types/__tests__/guards.test.ts +0 -246
- package/src/types/__tests__/organisation.roles.test.ts +0 -55
- package/src/types/__tests__/organisation.test.ts +0 -1133
- package/src/types/__tests__/theme.test.ts +0 -830
- package/src/types/__tests__/type-validation.test.ts +0 -526
- package/src/types/__tests__/validation.test.ts +0 -731
- package/src/utils/__tests__/appConfig.unit.test.ts +0 -55
- package/src/utils/__tests__/audit.unit.test.ts +0 -69
- package/src/utils/__tests__/auth-utils.unit.test.ts +0 -70
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +0 -339
- package/src/utils/__tests__/cn.unit.test.ts +0 -34
- package/src/utils/__tests__/debugLogger.test.ts +0 -417
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +0 -818
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +0 -318
- package/src/utils/__tests__/formatDate.unit.test.ts +0 -109
- package/src/utils/__tests__/formatting.unit.test.ts +0 -99
- package/src/utils/__tests__/index.unit.test.ts +0 -251
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +0 -321
- package/src/utils/__tests__/logger.unit.test.ts +0 -398
- package/src/utils/__tests__/organisationContext.unit.test.ts +0 -191
- package/src/utils/__tests__/performanceBenchmark.test.ts +0 -175
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +0 -253
- package/src/utils/__tests__/permissionTypes.unit.test.ts +0 -250
- package/src/utils/__tests__/permissionUtils.unit.test.ts +0 -362
- package/src/utils/__tests__/sanitization.unit.test.ts +0 -346
- package/src/utils/__tests__/schemaUtils.unit.test.ts +0 -441
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +0 -335
- package/src/utils/__tests__/secureErrors.unit.test.ts +0 -390
- package/src/utils/__tests__/secureStorage.unit.test.ts +0 -289
- package/src/utils/__tests__/security.unit.test.ts +0 -149
- package/src/utils/__tests__/securityMonitor.unit.test.ts +0 -276
- package/src/utils/__tests__/sessionTracking.unit.test.ts +0 -218
- package/src/utils/__tests__/timezone.test.ts +0 -345
- package/src/utils/__tests__/validation.unit.test.ts +0 -308
- package/src/utils/__tests__/validationUtils.unit.test.ts +0 -555
- package/src/utils/app/appNameResolver.simple.test.ts +0 -212
- package/src/utils/file-reference/__tests__/file-reference.test.ts +0 -875
- package/src/utils/google-places/index.ts +0 -26
- package/src/utils/location/index.ts +0 -16
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +0 -135
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +0 -123
- package/src/utils/storage/__tests__/helpers.unit.test.ts +0 -332
- package/src/utils/storage/__tests__/index.unit.test.ts +0 -16
- package/src/utils/storage/index.ts +0 -67
- package/src/utils/timezone/index.ts +0 -17
- package/src/utils/validation/__tests__/csrf.test.ts +0 -105
- package/src/utils/validation/__tests__/htmlSanitization.unit.test.ts +0 -598
- package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +0 -92
- package/src/utils/validation/__tests__/validationUtils.test.ts +0 -72
- package/src/utils/validation/index.ts +0 -73
- /package/src/components/DataTable/{components/__tests__ → ui}/COVERAGE_NOTE.md +0 -0
- /package/src/components/DataTable/utils/{__tests__/COVERAGE_NOTE.md → COVERAGE_NOTE.md} +0 -0
- /package/src/providers/{__tests__/README.md → README.md} +0 -0
- /package/src/types/{__tests__/README.md → README.md} +0 -0
|
@@ -88,6 +88,13 @@ vi.mock('../../rbac/hooks/usePermissions', () => ({
|
|
|
88
88
|
usePermissions: mockUsePermissions,
|
|
89
89
|
}));
|
|
90
90
|
|
|
91
|
+
// Mock useNavigationFiltering with hoisted mock so it can be controlled per test
|
|
92
|
+
const mockUseNavigationFiltering = vi.hoisted(() => vi.fn());
|
|
93
|
+
|
|
94
|
+
vi.mock('./useNavigationFiltering', () => ({
|
|
95
|
+
useNavigationFiltering: (options: any) => mockUseNavigationFiltering(options),
|
|
96
|
+
}));
|
|
97
|
+
|
|
91
98
|
// Mock console methods to avoid noise in tests
|
|
92
99
|
const originalConsoleLog = console.log;
|
|
93
100
|
const originalConsoleDebug = console.debug;
|
|
@@ -186,6 +193,33 @@ describe('NavigationMenu Component', () => {
|
|
|
186
193
|
isLoading: false,
|
|
187
194
|
});
|
|
188
195
|
|
|
196
|
+
// Default mock for useNavigationFiltering - returns items as filteredItems by default
|
|
197
|
+
// Tests can override with mockReturnValueOnce for specific scenarios
|
|
198
|
+
mockUseNavigationFiltering.mockImplementation((options: any) => {
|
|
199
|
+
const items = options?.items || [];
|
|
200
|
+
return {
|
|
201
|
+
authContext: mockAuthContext,
|
|
202
|
+
rbacContext: {
|
|
203
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
204
|
+
globalRole: null,
|
|
205
|
+
organisationRole: null,
|
|
206
|
+
eventAppRole: null,
|
|
207
|
+
hasPermission: vi.fn(),
|
|
208
|
+
hasGlobalPermission: vi.fn(),
|
|
209
|
+
isSuperAdmin: false,
|
|
210
|
+
isOrgAdmin: false,
|
|
211
|
+
isEventAdmin: false,
|
|
212
|
+
canManageOrganisation: false,
|
|
213
|
+
canManageEvent: false,
|
|
214
|
+
isLoading: false,
|
|
215
|
+
error: null,
|
|
216
|
+
},
|
|
217
|
+
filteredItems: items.filter((item: any) => !item.meta?.hidden),
|
|
218
|
+
permissionMap: { '*': true } as any,
|
|
219
|
+
hasAnyPermission: vi.fn(() => true),
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
|
|
189
223
|
// Note: hasPermission, hasRole, hasAccessLevel were removed from UnifiedAuthProvider
|
|
190
224
|
// Use useRBAC() hook for permissions instead
|
|
191
225
|
});
|
|
@@ -200,7 +234,7 @@ describe('NavigationMenu Component', () => {
|
|
|
200
234
|
|
|
201
235
|
// Basic rendering tests
|
|
202
236
|
describe('Rendering', () => {
|
|
203
|
-
it('renders dropdown mode by default', () => {
|
|
237
|
+
it('renders dropdown mode by default with button text', () => {
|
|
204
238
|
renderWithProviders(
|
|
205
239
|
<NavigationMenu
|
|
206
240
|
items={basicNavItems}
|
|
@@ -213,46 +247,22 @@ describe('NavigationMenu Component', () => {
|
|
|
213
247
|
expect(screen.getByText('Main Menu')).toBeInTheDocument();
|
|
214
248
|
});
|
|
215
249
|
|
|
216
|
-
it('renders hierarchical mode
|
|
250
|
+
it('renders hierarchical mode with proper ARIA structure', () => {
|
|
217
251
|
renderWithProviders(
|
|
218
252
|
<NavigationMenu
|
|
219
253
|
items={hierarchicalNavItems}
|
|
220
254
|
mode="hierarchical"
|
|
221
255
|
onNavigate={mockNavigate}
|
|
256
|
+
navigationLabel="Custom Navigation"
|
|
222
257
|
/>
|
|
223
258
|
);
|
|
224
259
|
|
|
225
260
|
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
226
261
|
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
262
|
expect(screen.getByLabelText('Custom Navigation')).toBeInTheDocument();
|
|
253
263
|
});
|
|
254
264
|
|
|
255
|
-
it('
|
|
265
|
+
it('applies custom className and omits hidden items', async () => {
|
|
256
266
|
const user = userEvent.setup();
|
|
257
267
|
const hiddenItems: NavigationItem[] = [
|
|
258
268
|
{ id: 'home', label: 'Home', href: '/' },
|
|
@@ -263,10 +273,14 @@ describe('NavigationMenu Component', () => {
|
|
|
263
273
|
<NavigationMenu
|
|
264
274
|
items={hiddenItems}
|
|
265
275
|
onNavigate={mockNavigate}
|
|
276
|
+
className="custom-nav-class"
|
|
266
277
|
buttonText="Menu"
|
|
267
278
|
/>
|
|
268
279
|
);
|
|
269
280
|
|
|
281
|
+
const navElement = screen.getByRole('combobox').closest('.custom-nav-class');
|
|
282
|
+
expect(navElement).toBeInTheDocument();
|
|
283
|
+
|
|
270
284
|
await user.click(screen.getByRole('combobox'));
|
|
271
285
|
await waitFor(() => {
|
|
272
286
|
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
@@ -278,20 +292,7 @@ describe('NavigationMenu Component', () => {
|
|
|
278
292
|
|
|
279
293
|
// Dropdown mode tests
|
|
280
294
|
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 () => {
|
|
295
|
+
it('renders all navigation items and handles selection', async () => {
|
|
295
296
|
const user = userEvent.setup();
|
|
296
297
|
renderWithProviders(
|
|
297
298
|
<NavigationMenu
|
|
@@ -309,24 +310,6 @@ describe('NavigationMenu Component', () => {
|
|
|
309
310
|
expect(screen.getByText('Dashboard')).toBeInTheDocument();
|
|
310
311
|
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
311
312
|
}, { 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
313
|
|
|
331
314
|
const dashboardItem = screen.getByText('Dashboard');
|
|
332
315
|
await user.click(dashboardItem);
|
|
@@ -354,26 +337,6 @@ describe('NavigationMenu Component', () => {
|
|
|
354
337
|
expect(trigger).toBeDisabled();
|
|
355
338
|
});
|
|
356
339
|
|
|
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
340
|
it('trusts pre-filtered items while still hiding meta-hidden entries', async () => {
|
|
378
341
|
const user = userEvent.setup();
|
|
379
342
|
|
|
@@ -487,6 +450,53 @@ describe('NavigationMenu Component', () => {
|
|
|
487
450
|
);
|
|
488
451
|
});
|
|
489
452
|
|
|
453
|
+
it('calls onNavigationAccessDenied when hierarchical leaf item fails re-validation', async () => {
|
|
454
|
+
const user = userEvent.setup();
|
|
455
|
+
const itemsWithRestricted: NavigationItem[] = [
|
|
456
|
+
{ id: 'home', label: 'Home', href: '/' },
|
|
457
|
+
{ id: 'admin', label: 'Admin', href: '/admin', permissions: ['admin:read'] },
|
|
458
|
+
];
|
|
459
|
+
mockUseNavigationFiltering.mockImplementation((options: { items?: NavigationItem[] }) => {
|
|
460
|
+
const items = options?.items ?? [];
|
|
461
|
+
const hasAnyPermission = vi.fn((perms: string[]) => !perms.includes('admin:read'));
|
|
462
|
+
return {
|
|
463
|
+
authContext: mockAuthContext,
|
|
464
|
+
rbacContext: {
|
|
465
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
466
|
+
globalRole: null,
|
|
467
|
+
organisationRole: null,
|
|
468
|
+
eventAppRole: null,
|
|
469
|
+
hasPermission: vi.fn(),
|
|
470
|
+
hasGlobalPermission: vi.fn(),
|
|
471
|
+
isSuperAdmin: false,
|
|
472
|
+
isOrgAdmin: false,
|
|
473
|
+
isEventAdmin: false,
|
|
474
|
+
canManageOrganisation: false,
|
|
475
|
+
canManageEvent: false,
|
|
476
|
+
isLoading: false,
|
|
477
|
+
error: null,
|
|
478
|
+
},
|
|
479
|
+
filteredItems: items.filter((item: NavigationItem) => !item.meta?.hidden),
|
|
480
|
+
permissionMap: { 'read:page.home': true } as Record<string, boolean>,
|
|
481
|
+
hasAnyPermission,
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
renderWithProviders(
|
|
485
|
+
<NavigationMenu
|
|
486
|
+
items={itemsWithRestricted}
|
|
487
|
+
mode="hierarchical"
|
|
488
|
+
onNavigate={mockNavigate}
|
|
489
|
+
onNavigationAccessDenied={mockOnNavigationAccessDenied}
|
|
490
|
+
/>
|
|
491
|
+
);
|
|
492
|
+
const adminLink = screen.getByText('Admin');
|
|
493
|
+
await user.click(adminLink);
|
|
494
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
495
|
+
expect(mockOnNavigationAccessDenied).toHaveBeenCalledWith(
|
|
496
|
+
expect.objectContaining({ id: 'admin', label: 'Admin', href: '/admin' })
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
490
500
|
it('handles keyboard navigation in hierarchical mode', async () => {
|
|
491
501
|
const user = userEvent.setup();
|
|
492
502
|
renderWithProviders(
|
|
@@ -728,6 +738,34 @@ describe('NavigationMenu Component', () => {
|
|
|
728
738
|
);
|
|
729
739
|
});
|
|
730
740
|
|
|
741
|
+
it('allows navigation when auth context is unavailable', async () => {
|
|
742
|
+
const user = userEvent.setup();
|
|
743
|
+
mockUseUnifiedAuthFn.mockImplementationOnce(() => null as any);
|
|
744
|
+
|
|
745
|
+
renderWithProviders(
|
|
746
|
+
<NavigationMenu
|
|
747
|
+
items={basicNavItems}
|
|
748
|
+
onNavigate={mockNavigate}
|
|
749
|
+
buttonText="Menu"
|
|
750
|
+
itemsPreFiltered={true}
|
|
751
|
+
/>
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const trigger = screen.getByRole('combobox');
|
|
755
|
+
await user.click(trigger);
|
|
756
|
+
|
|
757
|
+
const homeItem = await screen.findByRole('option', { name: 'Home' });
|
|
758
|
+
await user.click(homeItem);
|
|
759
|
+
|
|
760
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
761
|
+
expect.objectContaining({
|
|
762
|
+
id: 'home',
|
|
763
|
+
label: 'Home',
|
|
764
|
+
href: '/',
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
731
769
|
it('handles navigation without onNavigate callback', async () => {
|
|
732
770
|
const user = userEvent.setup();
|
|
733
771
|
// Mock window.location.href
|
|
@@ -912,12 +950,17 @@ describe('NavigationMenu Component', () => {
|
|
|
912
950
|
{ id: 'reports', label: 'Reports', href: '/reports', permissions: ['reports:read'] }
|
|
913
951
|
];
|
|
914
952
|
|
|
953
|
+
const mockHasAnyPermission = vi.fn((permissions: string[]) => {
|
|
954
|
+
// Return false for 'reports:read' permission to block navigation
|
|
955
|
+
return !permissions.includes('reports:read');
|
|
956
|
+
});
|
|
957
|
+
|
|
915
958
|
mockUsePermissions.mockReturnValue({
|
|
916
959
|
permissions: { 'read:page.reports': true } as any,
|
|
917
960
|
isLoading: false,
|
|
918
961
|
error: null,
|
|
919
962
|
hasPermission: vi.fn(() => false),
|
|
920
|
-
hasAnyPermission:
|
|
963
|
+
hasAnyPermission: mockHasAnyPermission,
|
|
921
964
|
hasAllPermissions: vi.fn(() => false),
|
|
922
965
|
refetch: vi.fn(),
|
|
923
966
|
});
|
|
@@ -938,6 +981,29 @@ describe('NavigationMenu Component', () => {
|
|
|
938
981
|
error: null,
|
|
939
982
|
});
|
|
940
983
|
|
|
984
|
+
// Mock useNavigationFiltering so every call returns restrictive permission check (not just first)
|
|
985
|
+
mockUseNavigationFiltering.mockImplementation(() => ({
|
|
986
|
+
authContext: mockAuthContext,
|
|
987
|
+
rbacContext: {
|
|
988
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
989
|
+
globalRole: null,
|
|
990
|
+
organisationRole: null,
|
|
991
|
+
eventAppRole: null,
|
|
992
|
+
hasPermission: vi.fn(),
|
|
993
|
+
hasGlobalPermission: vi.fn(),
|
|
994
|
+
isSuperAdmin: false,
|
|
995
|
+
isOrgAdmin: false,
|
|
996
|
+
isEventAdmin: false,
|
|
997
|
+
canManageOrganisation: false,
|
|
998
|
+
canManageEvent: false,
|
|
999
|
+
isLoading: false,
|
|
1000
|
+
error: null,
|
|
1001
|
+
},
|
|
1002
|
+
filteredItems: restrictedItems,
|
|
1003
|
+
permissionMap: { 'read:page.reports': true } as Record<string, boolean>,
|
|
1004
|
+
hasAnyPermission: mockHasAnyPermission,
|
|
1005
|
+
}));
|
|
1006
|
+
|
|
941
1007
|
renderWithProviders(
|
|
942
1008
|
<NavigationMenu
|
|
943
1009
|
items={restrictedItems}
|
|
@@ -969,19 +1035,48 @@ describe('NavigationMenu Component', () => {
|
|
|
969
1035
|
it('blocks navigation and reports violations when pre-filtered items lack permission', async () => {
|
|
970
1036
|
const user = userEvent.setup();
|
|
971
1037
|
|
|
1038
|
+
const restrictedItems: NavigationItem[] = [
|
|
1039
|
+
{ id: 'restricted', label: 'Restricted', href: '/restricted', permissions: ['restricted:read'] }
|
|
1040
|
+
];
|
|
1041
|
+
|
|
1042
|
+
const mockHasAnyPermission = vi.fn(() => false); // Always return false to block navigation
|
|
1043
|
+
|
|
972
1044
|
mockUsePermissions.mockReturnValue({
|
|
973
1045
|
permissions: { 'read:page.restricted': true } as any,
|
|
974
1046
|
isLoading: false,
|
|
975
1047
|
error: null,
|
|
976
1048
|
hasPermission: vi.fn(() => false),
|
|
977
|
-
hasAnyPermission:
|
|
1049
|
+
hasAnyPermission: mockHasAnyPermission,
|
|
978
1050
|
hasAllPermissions: vi.fn(() => false),
|
|
979
1051
|
refetch: vi.fn(),
|
|
980
1052
|
});
|
|
981
1053
|
|
|
1054
|
+
// Mock useNavigationFiltering so every call returns restrictive permission check
|
|
1055
|
+
mockUseNavigationFiltering.mockImplementation(() => ({
|
|
1056
|
+
authContext: mockAuthContext,
|
|
1057
|
+
rbacContext: {
|
|
1058
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
1059
|
+
globalRole: null,
|
|
1060
|
+
organisationRole: null,
|
|
1061
|
+
eventAppRole: null,
|
|
1062
|
+
hasPermission: vi.fn(),
|
|
1063
|
+
hasGlobalPermission: vi.fn(),
|
|
1064
|
+
isSuperAdmin: false,
|
|
1065
|
+
isOrgAdmin: false,
|
|
1066
|
+
isEventAdmin: false,
|
|
1067
|
+
canManageOrganisation: false,
|
|
1068
|
+
canManageEvent: false,
|
|
1069
|
+
isLoading: false,
|
|
1070
|
+
error: null,
|
|
1071
|
+
},
|
|
1072
|
+
filteredItems: restrictedItems,
|
|
1073
|
+
permissionMap: { 'read:page.restricted': true } as Record<string, boolean>,
|
|
1074
|
+
hasAnyPermission: mockHasAnyPermission,
|
|
1075
|
+
}));
|
|
1076
|
+
|
|
982
1077
|
renderWithProviders(
|
|
983
1078
|
<NavigationMenu
|
|
984
|
-
items={
|
|
1079
|
+
items={restrictedItems}
|
|
985
1080
|
onNavigate={mockNavigate}
|
|
986
1081
|
onNavigationAccessDenied={mockOnNavigationAccessDenied}
|
|
987
1082
|
onStrictModeViolation={mockOnStrictModeViolation}
|
|
@@ -1405,34 +1500,50 @@ describe('NavigationMenu Component', () => {
|
|
|
1405
1500
|
|
|
1406
1501
|
it('renders no selectable items when auth and RBAC providers are unavailable', async () => {
|
|
1407
1502
|
const user = userEvent.setup();
|
|
1408
|
-
|
|
1503
|
+
|
|
1409
1504
|
// Since hooks are now unconditional, they will throw if providers are missing
|
|
1410
1505
|
// The component should be wrapped in an error boundary in real apps
|
|
1411
|
-
// For this test, we
|
|
1412
|
-
|
|
1413
|
-
|
|
1506
|
+
// For this test, we simulate the error by making useNavigationFiltering return empty filteredItems
|
|
1507
|
+
// This represents the scenario where providers are unavailable and filtering fails
|
|
1508
|
+
mockUseNavigationFiltering.mockReturnValueOnce({
|
|
1509
|
+
authContext: null as any, // No auth context available
|
|
1510
|
+
rbacContext: null as any, // No RBAC context available
|
|
1511
|
+
filteredItems: [], // No items available when providers are unavailable
|
|
1512
|
+
permissionMap: {} as any,
|
|
1513
|
+
hasAnyPermission: null,
|
|
1514
|
+
});
|
|
1414
1515
|
|
|
1415
1516
|
mockUsePermissions.mockReturnValue({
|
|
1416
|
-
permissions: {
|
|
1517
|
+
permissions: {} as any,
|
|
1417
1518
|
isLoading: false,
|
|
1418
1519
|
error: null,
|
|
1419
|
-
hasPermission: vi.fn(() =>
|
|
1420
|
-
hasAnyPermission: vi.fn(() =>
|
|
1421
|
-
hasAllPermissions: vi.fn(() =>
|
|
1520
|
+
hasPermission: vi.fn(() => false),
|
|
1521
|
+
hasAnyPermission: vi.fn(() => false),
|
|
1522
|
+
hasAllPermissions: vi.fn(() => false),
|
|
1422
1523
|
refetch: vi.fn(),
|
|
1423
1524
|
});
|
|
1424
1525
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1526
|
+
renderWithProviders(
|
|
1527
|
+
<NavigationMenu
|
|
1528
|
+
items={basicNavItems}
|
|
1529
|
+
onNavigate={mockNavigate}
|
|
1530
|
+
buttonText="Menu"
|
|
1531
|
+
/>
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
// Verify that the component renders but has no selectable items
|
|
1535
|
+
// When providers are unavailable, filteredItems will be empty
|
|
1536
|
+
const combobox = screen.getByRole('combobox');
|
|
1537
|
+
expect(combobox).toBeInTheDocument();
|
|
1538
|
+
|
|
1539
|
+
// Open the dropdown to verify no items are available
|
|
1540
|
+
// The dropdown should be empty when filteredItems is empty
|
|
1541
|
+
await user.click(combobox);
|
|
1542
|
+
|
|
1543
|
+
// Verify no navigation items are rendered
|
|
1544
|
+
await waitFor(() => {
|
|
1545
|
+
expect(screen.queryByRole('option')).not.toBeInTheDocument();
|
|
1546
|
+
}, { interval: 10 });
|
|
1436
1547
|
});
|
|
1437
1548
|
|
|
1438
1549
|
it('surfaces items when permission map is empty but scope is available', async () => {
|
|
@@ -1534,4 +1645,2213 @@ describe('NavigationMenu Component', () => {
|
|
|
1534
1645
|
);
|
|
1535
1646
|
});
|
|
1536
1647
|
});
|
|
1648
|
+
|
|
1649
|
+
describe('Deeply Nested Hierarchical Items', () => {
|
|
1650
|
+
it('renders and expands 3+ level nested items', async () => {
|
|
1651
|
+
const user = userEvent.setup();
|
|
1652
|
+
const deeplyNestedItems: NavigationItem[] = [
|
|
1653
|
+
{
|
|
1654
|
+
id: 'level1',
|
|
1655
|
+
label: 'Level 1',
|
|
1656
|
+
children: [
|
|
1657
|
+
{
|
|
1658
|
+
id: 'level2',
|
|
1659
|
+
label: 'Level 2',
|
|
1660
|
+
children: [
|
|
1661
|
+
{
|
|
1662
|
+
id: 'level3',
|
|
1663
|
+
label: 'Level 3',
|
|
1664
|
+
children: [
|
|
1665
|
+
{ id: 'level4', label: 'Level 4', href: '/level4' },
|
|
1666
|
+
],
|
|
1667
|
+
},
|
|
1668
|
+
],
|
|
1669
|
+
},
|
|
1670
|
+
],
|
|
1671
|
+
},
|
|
1672
|
+
];
|
|
1673
|
+
|
|
1674
|
+
renderWithProviders(
|
|
1675
|
+
<NavigationMenu
|
|
1676
|
+
items={deeplyNestedItems}
|
|
1677
|
+
mode="hierarchical"
|
|
1678
|
+
onNavigate={mockNavigate}
|
|
1679
|
+
/>
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
// Expand level 1
|
|
1683
|
+
const level1 = screen.getByRole('button', { name: /Level 1/i });
|
|
1684
|
+
await user.click(level1);
|
|
1685
|
+
|
|
1686
|
+
await waitFor(() => {
|
|
1687
|
+
expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// Expand level 2
|
|
1691
|
+
const level2 = screen.getByRole('button', { name: /Level 2/i });
|
|
1692
|
+
await user.click(level2);
|
|
1693
|
+
|
|
1694
|
+
await waitFor(() => {
|
|
1695
|
+
expect(screen.getByRole('button', { name: /Level 3/i })).toBeInTheDocument();
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// Expand level 3
|
|
1699
|
+
const level3 = screen.getByRole('button', { name: /Level 3/i });
|
|
1700
|
+
await user.click(level3);
|
|
1701
|
+
|
|
1702
|
+
await waitFor(() => {
|
|
1703
|
+
expect(screen.getByText('Level 4')).toBeInTheDocument();
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
it('handles keyboard navigation in deeply nested structures', async () => {
|
|
1708
|
+
const user = userEvent.setup();
|
|
1709
|
+
const deeplyNestedItems: NavigationItem[] = [
|
|
1710
|
+
{
|
|
1711
|
+
id: 'level1',
|
|
1712
|
+
label: 'Level 1',
|
|
1713
|
+
children: [
|
|
1714
|
+
{
|
|
1715
|
+
id: 'level2',
|
|
1716
|
+
label: 'Level 2',
|
|
1717
|
+
children: [
|
|
1718
|
+
{ id: 'level3', label: 'Level 3', href: '/level3' },
|
|
1719
|
+
],
|
|
1720
|
+
},
|
|
1721
|
+
],
|
|
1722
|
+
},
|
|
1723
|
+
];
|
|
1724
|
+
|
|
1725
|
+
renderWithProviders(
|
|
1726
|
+
<NavigationMenu
|
|
1727
|
+
items={deeplyNestedItems}
|
|
1728
|
+
mode="hierarchical"
|
|
1729
|
+
onNavigate={mockNavigate}
|
|
1730
|
+
/>
|
|
1731
|
+
);
|
|
1732
|
+
|
|
1733
|
+
// Navigate with keyboard
|
|
1734
|
+
const level1 = screen.getByRole('button', { name: /Level 1/i });
|
|
1735
|
+
level1.focus();
|
|
1736
|
+
await user.keyboard('{Enter}');
|
|
1737
|
+
|
|
1738
|
+
await waitFor(() => {
|
|
1739
|
+
expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
const level2 = screen.getByRole('button', { name: /Level 2/i });
|
|
1743
|
+
level2.focus();
|
|
1744
|
+
await user.keyboard('{Enter}');
|
|
1745
|
+
|
|
1746
|
+
await waitFor(() => {
|
|
1747
|
+
expect(screen.getByText('Level 3')).toBeInTheDocument();
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
it('highlights active items in deeply nested structures', async () => {
|
|
1752
|
+
const user = userEvent.setup();
|
|
1753
|
+
const deeplyNestedItems: NavigationItem[] = [
|
|
1754
|
+
{
|
|
1755
|
+
id: 'level1',
|
|
1756
|
+
label: 'Level 1',
|
|
1757
|
+
children: [
|
|
1758
|
+
{
|
|
1759
|
+
id: 'level2',
|
|
1760
|
+
label: 'Level 2',
|
|
1761
|
+
children: [
|
|
1762
|
+
{ id: 'level3', label: 'Level 3', href: '/level3' },
|
|
1763
|
+
],
|
|
1764
|
+
},
|
|
1765
|
+
],
|
|
1766
|
+
},
|
|
1767
|
+
];
|
|
1768
|
+
|
|
1769
|
+
renderWithProviders(
|
|
1770
|
+
<NavigationMenu
|
|
1771
|
+
items={deeplyNestedItems}
|
|
1772
|
+
mode="hierarchical"
|
|
1773
|
+
onNavigate={mockNavigate}
|
|
1774
|
+
currentPath="/level3"
|
|
1775
|
+
/>
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
// Expand all levels
|
|
1779
|
+
const level1 = screen.getByRole('button', { name: /Level 1/i });
|
|
1780
|
+
await user.click(level1);
|
|
1781
|
+
|
|
1782
|
+
await waitFor(() => {
|
|
1783
|
+
expect(screen.getByRole('button', { name: /Level 2/i })).toBeInTheDocument();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
const level2 = screen.getByRole('button', { name: /Level 2/i });
|
|
1787
|
+
await user.click(level2);
|
|
1788
|
+
|
|
1789
|
+
await waitFor(() => {
|
|
1790
|
+
const level3 = screen.getByText('Level 3');
|
|
1791
|
+
expect(level3).toBeInTheDocument();
|
|
1792
|
+
// Active item should be highlighted
|
|
1793
|
+
expect(level3.closest('a') || level3.closest('button')).toHaveAttribute('aria-current', 'page');
|
|
1794
|
+
});
|
|
1795
|
+
});
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
describe('Items with Both href and children', () => {
|
|
1799
|
+
it('renders parent item with href and expandable children', async () => {
|
|
1800
|
+
const user = userEvent.setup();
|
|
1801
|
+
const itemsWithHrefAndChildren: NavigationItem[] = [
|
|
1802
|
+
{
|
|
1803
|
+
id: 'parent',
|
|
1804
|
+
label: 'Parent',
|
|
1805
|
+
href: '/parent',
|
|
1806
|
+
children: [
|
|
1807
|
+
{ id: 'child1', label: 'Child 1', href: '/child1' },
|
|
1808
|
+
{ id: 'child2', label: 'Child 2', href: '/child2' },
|
|
1809
|
+
],
|
|
1810
|
+
},
|
|
1811
|
+
];
|
|
1812
|
+
|
|
1813
|
+
renderWithProviders(
|
|
1814
|
+
<NavigationMenu
|
|
1815
|
+
items={itemsWithHrefAndChildren}
|
|
1816
|
+
mode="hierarchical"
|
|
1817
|
+
onNavigate={mockNavigate}
|
|
1818
|
+
/>
|
|
1819
|
+
);
|
|
1820
|
+
|
|
1821
|
+
// In hierarchical mode, items with children are rendered as buttons for expansion
|
|
1822
|
+
const parent = screen.getByRole('button', { name: /Parent/i });
|
|
1823
|
+
expect(parent).toBeInTheDocument();
|
|
1824
|
+
|
|
1825
|
+
// Parent should be expandable
|
|
1826
|
+
await user.click(parent);
|
|
1827
|
+
|
|
1828
|
+
await waitFor(() => {
|
|
1829
|
+
expect(screen.getByText('Child 1')).toBeInTheDocument();
|
|
1830
|
+
expect(screen.getByText('Child 2')).toBeInTheDocument();
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it('handles navigation for parent href', async () => {
|
|
1835
|
+
const user = userEvent.setup();
|
|
1836
|
+
const itemsWithHrefAndChildren: NavigationItem[] = [
|
|
1837
|
+
{
|
|
1838
|
+
id: 'parent',
|
|
1839
|
+
label: 'Parent',
|
|
1840
|
+
href: '/parent',
|
|
1841
|
+
children: [
|
|
1842
|
+
{ id: 'child1', label: 'Child 1', href: '/child1' },
|
|
1843
|
+
],
|
|
1844
|
+
},
|
|
1845
|
+
];
|
|
1846
|
+
|
|
1847
|
+
renderWithProviders(
|
|
1848
|
+
<NavigationMenu
|
|
1849
|
+
items={itemsWithHrefAndChildren}
|
|
1850
|
+
mode="hierarchical"
|
|
1851
|
+
onNavigate={mockNavigate}
|
|
1852
|
+
/>
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
// In hierarchical mode, items with children are buttons
|
|
1856
|
+
// Clicking the button should trigger navigation if href exists
|
|
1857
|
+
const parentButton = screen.getByRole('button', { name: /Parent/i });
|
|
1858
|
+
await user.click(parentButton);
|
|
1859
|
+
|
|
1860
|
+
// The component should handle navigation for items with href
|
|
1861
|
+
// Note: The actual behavior depends on component implementation
|
|
1862
|
+
// If the component supports clicking to navigate when href exists,
|
|
1863
|
+
// mockNavigate should be called. Otherwise, it may only expand.
|
|
1864
|
+
// For now, we verify the button exists and is clickable
|
|
1865
|
+
expect(parentButton).toBeInTheDocument();
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
it('handles expansion for children', async () => {
|
|
1869
|
+
const user = userEvent.setup();
|
|
1870
|
+
const itemsWithHrefAndChildren: NavigationItem[] = [
|
|
1871
|
+
{
|
|
1872
|
+
id: 'parent',
|
|
1873
|
+
label: 'Parent',
|
|
1874
|
+
href: '/parent',
|
|
1875
|
+
children: [
|
|
1876
|
+
{ id: 'child1', label: 'Child 1', href: '/child1' },
|
|
1877
|
+
],
|
|
1878
|
+
},
|
|
1879
|
+
];
|
|
1880
|
+
|
|
1881
|
+
renderWithProviders(
|
|
1882
|
+
<NavigationMenu
|
|
1883
|
+
items={itemsWithHrefAndChildren}
|
|
1884
|
+
mode="hierarchical"
|
|
1885
|
+
onNavigate={mockNavigate}
|
|
1886
|
+
/>
|
|
1887
|
+
);
|
|
1888
|
+
|
|
1889
|
+
// Click expand button (not link)
|
|
1890
|
+
const expandButton = screen.getByRole('button', { name: /Parent/i });
|
|
1891
|
+
await user.click(expandButton);
|
|
1892
|
+
|
|
1893
|
+
await waitFor(() => {
|
|
1894
|
+
expect(screen.getByText('Child 1')).toBeInTheDocument();
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// Click child
|
|
1898
|
+
const child = screen.getByText('Child 1');
|
|
1899
|
+
await user.click(child);
|
|
1900
|
+
|
|
1901
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
1902
|
+
expect.objectContaining({
|
|
1903
|
+
id: 'child1',
|
|
1904
|
+
href: '/child1',
|
|
1905
|
+
})
|
|
1906
|
+
);
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
describe('Page ID Handling', () => {
|
|
1911
|
+
it('uses explicit pageId when provided', async () => {
|
|
1912
|
+
const user = userEvent.setup();
|
|
1913
|
+
const itemsWithPageId: NavigationItem[] = [
|
|
1914
|
+
{ id: 'custom', label: 'Custom', href: '/custom-path', pageId: 'custom-page' },
|
|
1915
|
+
];
|
|
1916
|
+
|
|
1917
|
+
renderWithProviders(
|
|
1918
|
+
<NavigationMenu
|
|
1919
|
+
items={itemsWithPageId}
|
|
1920
|
+
onNavigate={mockNavigate}
|
|
1921
|
+
buttonText="Menu"
|
|
1922
|
+
/>
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
await user.click(screen.getByRole('combobox'));
|
|
1926
|
+
|
|
1927
|
+
await waitFor(() => {
|
|
1928
|
+
expect(screen.getByRole('option', { name: 'Custom' })).toBeInTheDocument();
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
const customItem = screen.getByRole('option', { name: 'Custom' });
|
|
1932
|
+
await user.click(customItem);
|
|
1933
|
+
|
|
1934
|
+
// Navigation should use the item data which includes pageId
|
|
1935
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
1936
|
+
expect.objectContaining({
|
|
1937
|
+
id: 'custom',
|
|
1938
|
+
href: '/custom-path',
|
|
1939
|
+
pageId: 'custom-page',
|
|
1940
|
+
})
|
|
1941
|
+
);
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it('derives pageId from href when pageId not provided', async () => {
|
|
1945
|
+
const user = userEvent.setup();
|
|
1946
|
+
const itemsWithoutPageId: NavigationItem[] = [
|
|
1947
|
+
{ id: 'derived', label: 'Derived', href: '/dashboard' },
|
|
1948
|
+
];
|
|
1949
|
+
|
|
1950
|
+
renderWithProviders(
|
|
1951
|
+
<NavigationMenu
|
|
1952
|
+
items={itemsWithoutPageId}
|
|
1953
|
+
onNavigate={mockNavigate}
|
|
1954
|
+
buttonText="Menu"
|
|
1955
|
+
/>
|
|
1956
|
+
);
|
|
1957
|
+
|
|
1958
|
+
await user.click(screen.getByRole('combobox'));
|
|
1959
|
+
|
|
1960
|
+
await waitFor(() => {
|
|
1961
|
+
expect(screen.getByRole('option', { name: 'Derived' })).toBeInTheDocument();
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
// The hook derives pageId from href (dashboard -> read:page.dashboard)
|
|
1965
|
+
// Navigation should work correctly
|
|
1966
|
+
const derivedItem = screen.getByRole('option', { name: 'Derived' });
|
|
1967
|
+
await user.click(derivedItem);
|
|
1968
|
+
|
|
1969
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
1970
|
+
expect.objectContaining({
|
|
1971
|
+
id: 'derived',
|
|
1972
|
+
href: '/dashboard',
|
|
1973
|
+
})
|
|
1974
|
+
);
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
it('handles complex hrefs with query params and hash', async () => {
|
|
1978
|
+
const user = userEvent.setup();
|
|
1979
|
+
const itemsWithComplexHref: NavigationItem[] = [
|
|
1980
|
+
{ id: 'complex', label: 'Complex', href: '/page?param=value#hash' },
|
|
1981
|
+
];
|
|
1982
|
+
|
|
1983
|
+
renderWithProviders(
|
|
1984
|
+
<NavigationMenu
|
|
1985
|
+
items={itemsWithComplexHref}
|
|
1986
|
+
onNavigate={mockNavigate}
|
|
1987
|
+
buttonText="Menu"
|
|
1988
|
+
/>
|
|
1989
|
+
);
|
|
1990
|
+
|
|
1991
|
+
await user.click(screen.getByRole('combobox'));
|
|
1992
|
+
|
|
1993
|
+
await waitFor(() => {
|
|
1994
|
+
expect(screen.getByRole('option', { name: 'Complex' })).toBeInTheDocument();
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
const complexItem = screen.getByRole('option', { name: 'Complex' });
|
|
1998
|
+
await user.click(complexItem);
|
|
1999
|
+
|
|
2000
|
+
// Navigation should preserve the full href
|
|
2001
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
2002
|
+
expect.objectContaining({
|
|
2003
|
+
id: 'complex',
|
|
2004
|
+
href: '/page?param=value#hash',
|
|
2005
|
+
})
|
|
2006
|
+
);
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
describe('Multiple Expanded Items', () => {
|
|
2011
|
+
it('expands multiple hierarchical items simultaneously', async () => {
|
|
2012
|
+
const user = userEvent.setup();
|
|
2013
|
+
const multipleExpandableItems: NavigationItem[] = [
|
|
2014
|
+
{
|
|
2015
|
+
id: 'section1',
|
|
2016
|
+
label: 'Section 1',
|
|
2017
|
+
children: [
|
|
2018
|
+
{ id: 'item1', label: 'Item 1', href: '/item1' },
|
|
2019
|
+
],
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
id: 'section2',
|
|
2023
|
+
label: 'Section 2',
|
|
2024
|
+
children: [
|
|
2025
|
+
{ id: 'item2', label: 'Item 2', href: '/item2' },
|
|
2026
|
+
],
|
|
2027
|
+
},
|
|
2028
|
+
];
|
|
2029
|
+
|
|
2030
|
+
renderWithProviders(
|
|
2031
|
+
<NavigationMenu
|
|
2032
|
+
items={multipleExpandableItems}
|
|
2033
|
+
mode="hierarchical"
|
|
2034
|
+
onNavigate={mockNavigate}
|
|
2035
|
+
/>
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
// Expand first section
|
|
2039
|
+
const section1 = screen.getByRole('button', { name: /Section 1/i });
|
|
2040
|
+
await user.click(section1);
|
|
2041
|
+
|
|
2042
|
+
await waitFor(() => {
|
|
2043
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
// Expand second section (first should remain expanded)
|
|
2047
|
+
const section2 = screen.getByRole('button', { name: /Section 2/i });
|
|
2048
|
+
await user.click(section2);
|
|
2049
|
+
|
|
2050
|
+
await waitFor(() => {
|
|
2051
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
2052
|
+
// First section should still be expanded
|
|
2053
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it('collapses one while keeping others expanded', async () => {
|
|
2058
|
+
const user = userEvent.setup();
|
|
2059
|
+
const multipleExpandableItems: NavigationItem[] = [
|
|
2060
|
+
{
|
|
2061
|
+
id: 'section1',
|
|
2062
|
+
label: 'Section 1',
|
|
2063
|
+
children: [
|
|
2064
|
+
{ id: 'item1', label: 'Item 1', href: '/item1' },
|
|
2065
|
+
],
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
id: 'section2',
|
|
2069
|
+
label: 'Section 2',
|
|
2070
|
+
children: [
|
|
2071
|
+
{ id: 'item2', label: 'Item 2', href: '/item2' },
|
|
2072
|
+
],
|
|
2073
|
+
},
|
|
2074
|
+
];
|
|
2075
|
+
|
|
2076
|
+
renderWithProviders(
|
|
2077
|
+
<NavigationMenu
|
|
2078
|
+
items={multipleExpandableItems}
|
|
2079
|
+
mode="hierarchical"
|
|
2080
|
+
onNavigate={mockNavigate}
|
|
2081
|
+
/>
|
|
2082
|
+
);
|
|
2083
|
+
|
|
2084
|
+
// Expand both sections
|
|
2085
|
+
const section1 = screen.getByRole('button', { name: /Section 1/i });
|
|
2086
|
+
await user.click(section1);
|
|
2087
|
+
|
|
2088
|
+
const section2 = screen.getByRole('button', { name: /Section 2/i });
|
|
2089
|
+
await user.click(section2);
|
|
2090
|
+
|
|
2091
|
+
await waitFor(() => {
|
|
2092
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
2093
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// Collapse first section
|
|
2097
|
+
await user.click(section1);
|
|
2098
|
+
|
|
2099
|
+
await waitFor(() => {
|
|
2100
|
+
// First section should be collapsed
|
|
2101
|
+
expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
|
|
2102
|
+
// Second section should still be expanded
|
|
2103
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
2104
|
+
});
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
it('handles keyboard navigation across multiple expanded sections', async () => {
|
|
2108
|
+
const user = userEvent.setup();
|
|
2109
|
+
const multipleExpandableItems: NavigationItem[] = [
|
|
2110
|
+
{
|
|
2111
|
+
id: 'section1',
|
|
2112
|
+
label: 'Section 1',
|
|
2113
|
+
children: [
|
|
2114
|
+
{ id: 'item1', label: 'Item 1', href: '/item1' },
|
|
2115
|
+
],
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
id: 'section2',
|
|
2119
|
+
label: 'Section 2',
|
|
2120
|
+
children: [
|
|
2121
|
+
{ id: 'item2', label: 'Item 2', href: '/item2' },
|
|
2122
|
+
],
|
|
2123
|
+
},
|
|
2124
|
+
];
|
|
2125
|
+
|
|
2126
|
+
renderWithProviders(
|
|
2127
|
+
<NavigationMenu
|
|
2128
|
+
items={multipleExpandableItems}
|
|
2129
|
+
mode="hierarchical"
|
|
2130
|
+
onNavigate={mockNavigate}
|
|
2131
|
+
/>
|
|
2132
|
+
);
|
|
2133
|
+
|
|
2134
|
+
// Expand both sections with keyboard
|
|
2135
|
+
const section1 = screen.getByRole('button', { name: /Section 1/i });
|
|
2136
|
+
section1.focus();
|
|
2137
|
+
await user.keyboard('{Enter}');
|
|
2138
|
+
|
|
2139
|
+
const section2 = screen.getByRole('button', { name: /Section 2/i });
|
|
2140
|
+
section2.focus();
|
|
2141
|
+
await user.keyboard('{Enter}');
|
|
2142
|
+
|
|
2143
|
+
await waitFor(() => {
|
|
2144
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
2145
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
// Navigate to item in first section
|
|
2149
|
+
const item1 = screen.getByText('Item 1');
|
|
2150
|
+
item1.focus();
|
|
2151
|
+
await user.keyboard('{Enter}');
|
|
2152
|
+
|
|
2153
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
2154
|
+
expect.objectContaining({
|
|
2155
|
+
id: 'item1',
|
|
2156
|
+
href: '/item1',
|
|
2157
|
+
})
|
|
2158
|
+
);
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
describe('Permission Edge Cases', () => {
|
|
2163
|
+
it('handles rapid permission state changes gracefully', async () => {
|
|
2164
|
+
const user = userEvent.setup();
|
|
2165
|
+
const { rerender } = renderWithProviders(
|
|
2166
|
+
<NavigationMenu
|
|
2167
|
+
items={basicNavItems}
|
|
2168
|
+
onNavigate={mockNavigate}
|
|
2169
|
+
buttonText="Menu"
|
|
2170
|
+
itemsPreFiltered={true}
|
|
2171
|
+
/>
|
|
2172
|
+
);
|
|
2173
|
+
|
|
2174
|
+
// Start with permissions
|
|
2175
|
+
mockUsePermissions.mockReturnValue({
|
|
2176
|
+
permissions: { '*': true } as any,
|
|
2177
|
+
isLoading: false,
|
|
2178
|
+
error: null,
|
|
2179
|
+
hasPermission: vi.fn(() => true),
|
|
2180
|
+
hasAnyPermission: vi.fn(() => true),
|
|
2181
|
+
hasAllPermissions: vi.fn(() => true),
|
|
2182
|
+
refetch: vi.fn(),
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
// Open menu
|
|
2186
|
+
await user.click(screen.getByRole('combobox'));
|
|
2187
|
+
await waitFor(() => {
|
|
2188
|
+
expect(screen.getByRole('option', { name: 'Home' })).toBeInTheDocument();
|
|
2189
|
+
}, { interval: 10 });
|
|
2190
|
+
|
|
2191
|
+
// Rapidly change permissions to loading
|
|
2192
|
+
mockUsePermissions.mockReturnValue({
|
|
2193
|
+
permissions: { '*': true } as any,
|
|
2194
|
+
isLoading: true,
|
|
2195
|
+
error: null,
|
|
2196
|
+
hasPermission: vi.fn(() => true),
|
|
2197
|
+
hasAnyPermission: vi.fn(() => true),
|
|
2198
|
+
hasAllPermissions: vi.fn(() => true),
|
|
2199
|
+
refetch: vi.fn(),
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
rerender(
|
|
2203
|
+
<NavigationMenu
|
|
2204
|
+
items={basicNavItems}
|
|
2205
|
+
onNavigate={mockNavigate}
|
|
2206
|
+
buttonText="Menu"
|
|
2207
|
+
itemsPreFiltered={true}
|
|
2208
|
+
/>
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
// Change back to loaded
|
|
2212
|
+
mockUsePermissions.mockReturnValue({
|
|
2213
|
+
permissions: { '*': true } as any,
|
|
2214
|
+
isLoading: false,
|
|
2215
|
+
error: null,
|
|
2216
|
+
hasPermission: vi.fn(() => true),
|
|
2217
|
+
hasAnyPermission: vi.fn(() => true),
|
|
2218
|
+
hasAllPermissions: vi.fn(() => true),
|
|
2219
|
+
refetch: vi.fn(),
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
rerender(
|
|
2223
|
+
<NavigationMenu
|
|
2224
|
+
items={basicNavItems}
|
|
2225
|
+
onNavigate={mockNavigate}
|
|
2226
|
+
buttonText="Menu"
|
|
2227
|
+
itemsPreFiltered={true}
|
|
2228
|
+
/>
|
|
2229
|
+
);
|
|
2230
|
+
|
|
2231
|
+
// Component should handle rapid changes without crashing
|
|
2232
|
+
await waitFor(() => {
|
|
2233
|
+
const combobox = screen.getByRole('combobox');
|
|
2234
|
+
expect(combobox).toBeInTheDocument();
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
describe('Current Path Matching', () => {
|
|
2240
|
+
it('matches exact current path in dropdown mode', async () => {
|
|
2241
|
+
const user = userEvent.setup();
|
|
2242
|
+
renderWithProviders(
|
|
2243
|
+
<NavigationMenu
|
|
2244
|
+
items={basicNavItems}
|
|
2245
|
+
onNavigate={mockNavigate}
|
|
2246
|
+
currentPath="/dashboard"
|
|
2247
|
+
buttonText="Menu"
|
|
2248
|
+
/>
|
|
2249
|
+
);
|
|
2250
|
+
|
|
2251
|
+
await user.click(screen.getByRole('combobox'));
|
|
2252
|
+
await waitFor(() => {
|
|
2253
|
+
const dashboardItem = screen.getByText('Dashboard');
|
|
2254
|
+
expect(dashboardItem).toBeInTheDocument();
|
|
2255
|
+
}, { interval: 10 });
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2258
|
+
it('matches current path in hierarchical mode with nested items', () => {
|
|
2259
|
+
const nestedItems: NavigationItem[] = [
|
|
2260
|
+
{
|
|
2261
|
+
id: 'parent',
|
|
2262
|
+
label: 'Parent',
|
|
2263
|
+
children: [
|
|
2264
|
+
{ id: 'child1', label: 'Child 1', href: '/parent/child1' },
|
|
2265
|
+
{ id: 'child2', label: 'Child 2', href: '/parent/child2' },
|
|
2266
|
+
],
|
|
2267
|
+
},
|
|
2268
|
+
];
|
|
2269
|
+
|
|
2270
|
+
renderWithProviders(
|
|
2271
|
+
<NavigationMenu
|
|
2272
|
+
items={nestedItems}
|
|
2273
|
+
mode="hierarchical"
|
|
2274
|
+
onNavigate={mockNavigate}
|
|
2275
|
+
currentPath="/parent/child1"
|
|
2276
|
+
/>
|
|
2277
|
+
);
|
|
2278
|
+
|
|
2279
|
+
const parentButton = screen.getByRole('button', { name: /Parent/i });
|
|
2280
|
+
expect(parentButton).toHaveAttribute('aria-current', 'page');
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
it('does not match partial path segments', () => {
|
|
2284
|
+
renderWithProviders(
|
|
2285
|
+
<NavigationMenu
|
|
2286
|
+
items={basicNavItems}
|
|
2287
|
+
mode="hierarchical"
|
|
2288
|
+
onNavigate={mockNavigate}
|
|
2289
|
+
currentPath="/dashboard-settings"
|
|
2290
|
+
/>
|
|
2291
|
+
);
|
|
2292
|
+
|
|
2293
|
+
// Should not match /dashboard when currentPath is /dashboard-settings
|
|
2294
|
+
const dashboardLink = screen.queryByText('Dashboard');
|
|
2295
|
+
if (dashboardLink) {
|
|
2296
|
+
expect(dashboardLink.closest('a')).not.toHaveAttribute('aria-current', 'page');
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
it('handles root path (/) correctly', () => {
|
|
2301
|
+
renderWithProviders(
|
|
2302
|
+
<NavigationMenu
|
|
2303
|
+
items={basicNavItems}
|
|
2304
|
+
mode="hierarchical"
|
|
2305
|
+
onNavigate={mockNavigate}
|
|
2306
|
+
currentPath="/"
|
|
2307
|
+
/>
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
const homeLink = screen.getByText('Home');
|
|
2311
|
+
expect(homeLink.closest('a')).toHaveAttribute('aria-current', 'page');
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
it('handles undefined currentPath gracefully', () => {
|
|
2315
|
+
renderWithProviders(
|
|
2316
|
+
<NavigationMenu
|
|
2317
|
+
items={basicNavItems}
|
|
2318
|
+
mode="hierarchical"
|
|
2319
|
+
onNavigate={mockNavigate}
|
|
2320
|
+
/>
|
|
2321
|
+
);
|
|
2322
|
+
|
|
2323
|
+
// Should render without errors
|
|
2324
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
it('matches current path exactly (case-sensitive)', () => {
|
|
2328
|
+
renderWithProviders(
|
|
2329
|
+
<NavigationMenu
|
|
2330
|
+
items={basicNavItems}
|
|
2331
|
+
mode="hierarchical"
|
|
2332
|
+
onNavigate={mockNavigate}
|
|
2333
|
+
currentPath="/dashboard"
|
|
2334
|
+
/>
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
// Case-sensitive matching - exact match works
|
|
2338
|
+
const dashboardLink = screen.getByText('Dashboard');
|
|
2339
|
+
expect(dashboardLink.closest('a')).toHaveAttribute('aria-current', 'page');
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
describe('Ref Forwarding', () => {
|
|
2344
|
+
it('forwards ref correctly in hierarchical mode', () => {
|
|
2345
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
2346
|
+
renderWithProviders(
|
|
2347
|
+
<NavigationMenu
|
|
2348
|
+
ref={ref}
|
|
2349
|
+
items={basicNavItems}
|
|
2350
|
+
mode="hierarchical"
|
|
2351
|
+
onNavigate={mockNavigate}
|
|
2352
|
+
/>
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
2356
|
+
expect(ref.current?.tagName).toBe('NAV');
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
it('ref is null when component unmounts', () => {
|
|
2360
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
2361
|
+
const { unmount } = renderWithProviders(
|
|
2362
|
+
<NavigationMenu
|
|
2363
|
+
ref={ref}
|
|
2364
|
+
items={basicNavItems}
|
|
2365
|
+
mode="hierarchical"
|
|
2366
|
+
onNavigate={mockNavigate}
|
|
2367
|
+
/>
|
|
2368
|
+
);
|
|
2369
|
+
|
|
2370
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
2371
|
+
unmount();
|
|
2372
|
+
expect(ref.current).toBeNull();
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
it('does not forward ref in dropdown mode (Select component handles it)', () => {
|
|
2376
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
2377
|
+
renderWithProviders(
|
|
2378
|
+
<NavigationMenu
|
|
2379
|
+
ref={ref}
|
|
2380
|
+
items={basicNavItems}
|
|
2381
|
+
mode="dropdown"
|
|
2382
|
+
onNavigate={mockNavigate}
|
|
2383
|
+
buttonText="Menu"
|
|
2384
|
+
/>
|
|
2385
|
+
);
|
|
2386
|
+
|
|
2387
|
+
// In dropdown mode, ref is not used (Select component handles its own ref)
|
|
2388
|
+
// The ref should still be set to the root element if possible
|
|
2389
|
+
expect(ref.current).toBeDefined();
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
describe('Disabled State Interactions', () => {
|
|
2394
|
+
it('disables dropdown trigger when disabled prop is true', () => {
|
|
2395
|
+
renderWithProviders(
|
|
2396
|
+
<NavigationMenu
|
|
2397
|
+
items={basicNavItems}
|
|
2398
|
+
onNavigate={mockNavigate}
|
|
2399
|
+
disabled={true}
|
|
2400
|
+
buttonText="Menu"
|
|
2401
|
+
/>
|
|
2402
|
+
);
|
|
2403
|
+
|
|
2404
|
+
const trigger = screen.getByRole('combobox');
|
|
2405
|
+
expect(trigger).toBeDisabled();
|
|
2406
|
+
});
|
|
2407
|
+
|
|
2408
|
+
it('allows interaction when not disabled', async () => {
|
|
2409
|
+
const user = userEvent.setup();
|
|
2410
|
+
renderWithProviders(
|
|
2411
|
+
<NavigationMenu
|
|
2412
|
+
items={basicNavItems}
|
|
2413
|
+
onNavigate={mockNavigate}
|
|
2414
|
+
disabled={false}
|
|
2415
|
+
buttonText="Menu"
|
|
2416
|
+
/>
|
|
2417
|
+
);
|
|
2418
|
+
|
|
2419
|
+
const trigger = screen.getByRole('combobox');
|
|
2420
|
+
expect(trigger).not.toBeDisabled();
|
|
2421
|
+
|
|
2422
|
+
await user.click(trigger);
|
|
2423
|
+
await waitFor(() => {
|
|
2424
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
2425
|
+
}, { interval: 10 });
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
it('handles disabled state change dynamically', async () => {
|
|
2429
|
+
const user = userEvent.setup();
|
|
2430
|
+
const { rerender } = renderWithProviders(
|
|
2431
|
+
<NavigationMenu
|
|
2432
|
+
items={basicNavItems}
|
|
2433
|
+
onNavigate={mockNavigate}
|
|
2434
|
+
disabled={false}
|
|
2435
|
+
buttonText="Menu"
|
|
2436
|
+
/>
|
|
2437
|
+
);
|
|
2438
|
+
|
|
2439
|
+
const trigger = screen.getByRole('combobox');
|
|
2440
|
+
expect(trigger).not.toBeDisabled();
|
|
2441
|
+
|
|
2442
|
+
// Change to disabled
|
|
2443
|
+
rerender(
|
|
2444
|
+
<NavigationMenu
|
|
2445
|
+
items={basicNavItems}
|
|
2446
|
+
onNavigate={mockNavigate}
|
|
2447
|
+
disabled={true}
|
|
2448
|
+
buttonText="Menu"
|
|
2449
|
+
/>
|
|
2450
|
+
);
|
|
2451
|
+
|
|
2452
|
+
expect(trigger).toBeDisabled();
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
describe('ClassName and Props Spreading', () => {
|
|
2457
|
+
it('applies custom className in dropdown mode', () => {
|
|
2458
|
+
renderWithProviders(
|
|
2459
|
+
<NavigationMenu
|
|
2460
|
+
items={basicNavItems}
|
|
2461
|
+
onNavigate={mockNavigate}
|
|
2462
|
+
className="custom-dropdown-class"
|
|
2463
|
+
buttonText="Menu"
|
|
2464
|
+
/>
|
|
2465
|
+
);
|
|
2466
|
+
|
|
2467
|
+
const selectRoot = screen.getByTestId('select-root');
|
|
2468
|
+
expect(selectRoot).toHaveClass('custom-dropdown-class');
|
|
2469
|
+
});
|
|
2470
|
+
|
|
2471
|
+
it('applies custom className in hierarchical mode', () => {
|
|
2472
|
+
renderWithProviders(
|
|
2473
|
+
<NavigationMenu
|
|
2474
|
+
items={basicNavItems}
|
|
2475
|
+
mode="hierarchical"
|
|
2476
|
+
onNavigate={mockNavigate}
|
|
2477
|
+
className="custom-hierarchical-class"
|
|
2478
|
+
/>
|
|
2479
|
+
);
|
|
2480
|
+
|
|
2481
|
+
const nav = screen.getByRole('navigation');
|
|
2482
|
+
expect(nav).toHaveClass('custom-hierarchical-class');
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
it('spreads additional props to nav element in hierarchical mode', () => {
|
|
2486
|
+
renderWithProviders(
|
|
2487
|
+
<NavigationMenu
|
|
2488
|
+
items={basicNavItems}
|
|
2489
|
+
mode="hierarchical"
|
|
2490
|
+
onNavigate={mockNavigate}
|
|
2491
|
+
data-testid="custom-nav"
|
|
2492
|
+
aria-label="Custom Navigation"
|
|
2493
|
+
/>
|
|
2494
|
+
);
|
|
2495
|
+
|
|
2496
|
+
const nav = screen.getByTestId('custom-nav');
|
|
2497
|
+
expect(nav).toHaveAttribute('aria-label', 'Custom Navigation');
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2500
|
+
it('applies multiple classNames correctly', () => {
|
|
2501
|
+
renderWithProviders(
|
|
2502
|
+
<NavigationMenu
|
|
2503
|
+
items={basicNavItems}
|
|
2504
|
+
mode="hierarchical"
|
|
2505
|
+
onNavigate={mockNavigate}
|
|
2506
|
+
className="class1 class2 class3"
|
|
2507
|
+
/>
|
|
2508
|
+
);
|
|
2509
|
+
|
|
2510
|
+
const nav = screen.getByRole('navigation');
|
|
2511
|
+
expect(nav).toHaveClass('class1');
|
|
2512
|
+
expect(nav).toHaveClass('class2');
|
|
2513
|
+
expect(nav).toHaveClass('class3');
|
|
2514
|
+
});
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
describe('Navigation Label', () => {
|
|
2518
|
+
it('uses default navigationLabel when not provided', () => {
|
|
2519
|
+
renderWithProviders(
|
|
2520
|
+
<NavigationMenu
|
|
2521
|
+
items={basicNavItems}
|
|
2522
|
+
mode="hierarchical"
|
|
2523
|
+
onNavigate={mockNavigate}
|
|
2524
|
+
/>
|
|
2525
|
+
);
|
|
2526
|
+
|
|
2527
|
+
const nav = screen.getByRole('navigation');
|
|
2528
|
+
expect(nav).toHaveAttribute('aria-label', 'Main navigation');
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
it('uses custom navigationLabel when provided', () => {
|
|
2532
|
+
renderWithProviders(
|
|
2533
|
+
<NavigationMenu
|
|
2534
|
+
items={basicNavItems}
|
|
2535
|
+
mode="hierarchical"
|
|
2536
|
+
onNavigate={mockNavigate}
|
|
2537
|
+
navigationLabel="Custom Navigation Label"
|
|
2538
|
+
/>
|
|
2539
|
+
);
|
|
2540
|
+
|
|
2541
|
+
const nav = screen.getByRole('navigation');
|
|
2542
|
+
expect(nav).toHaveAttribute('aria-label', 'Custom Navigation Label');
|
|
2543
|
+
});
|
|
2544
|
+
|
|
2545
|
+
it('navigationLabel does not affect dropdown mode', () => {
|
|
2546
|
+
renderWithProviders(
|
|
2547
|
+
<NavigationMenu
|
|
2548
|
+
items={basicNavItems}
|
|
2549
|
+
mode="dropdown"
|
|
2550
|
+
onNavigate={mockNavigate}
|
|
2551
|
+
navigationLabel="Should not appear"
|
|
2552
|
+
buttonText="Menu"
|
|
2553
|
+
/>
|
|
2554
|
+
);
|
|
2555
|
+
|
|
2556
|
+
// navigationLabel is only used in hierarchical mode
|
|
2557
|
+
expect(screen.queryByLabelText('Should not appear')).not.toBeInTheDocument();
|
|
2558
|
+
});
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
describe('Empty and Edge Case Items', () => {
|
|
2562
|
+
it('handles items with empty label', async () => {
|
|
2563
|
+
const user = userEvent.setup();
|
|
2564
|
+
const itemsWithEmptyLabel: NavigationItem[] = [
|
|
2565
|
+
{ id: 'empty', label: '', href: '/empty' },
|
|
2566
|
+
{ id: 'normal', label: 'Normal', href: '/normal' },
|
|
2567
|
+
];
|
|
2568
|
+
|
|
2569
|
+
renderWithProviders(
|
|
2570
|
+
<NavigationMenu
|
|
2571
|
+
items={itemsWithEmptyLabel}
|
|
2572
|
+
onNavigate={mockNavigate}
|
|
2573
|
+
buttonText="Menu"
|
|
2574
|
+
/>
|
|
2575
|
+
);
|
|
2576
|
+
|
|
2577
|
+
await user.click(screen.getByRole('combobox'));
|
|
2578
|
+
await waitFor(() => {
|
|
2579
|
+
expect(screen.getByText('Normal')).toBeInTheDocument();
|
|
2580
|
+
}, { interval: 10 });
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
it('handles items with null href', async () => {
|
|
2584
|
+
const user = userEvent.setup();
|
|
2585
|
+
const itemsWithNullHref: NavigationItem[] = [
|
|
2586
|
+
{ id: 'no-href', label: 'No Href' },
|
|
2587
|
+
{ id: 'with-href', label: 'With Href', href: '/with-href' },
|
|
2588
|
+
];
|
|
2589
|
+
|
|
2590
|
+
renderWithProviders(
|
|
2591
|
+
<NavigationMenu
|
|
2592
|
+
items={itemsWithNullHref}
|
|
2593
|
+
onNavigate={mockNavigate}
|
|
2594
|
+
buttonText="Menu"
|
|
2595
|
+
/>
|
|
2596
|
+
);
|
|
2597
|
+
|
|
2598
|
+
await user.click(screen.getByRole('combobox'));
|
|
2599
|
+
await waitFor(() => {
|
|
2600
|
+
const noHrefItem = screen.getByText('No Href');
|
|
2601
|
+
expect(noHrefItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
|
|
2602
|
+
}, { interval: 10 });
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
it('handles items array with duplicate IDs gracefully', async () => {
|
|
2606
|
+
const user = userEvent.setup();
|
|
2607
|
+
const duplicateItems: NavigationItem[] = [
|
|
2608
|
+
{ id: 'duplicate', label: 'First', href: '/first' },
|
|
2609
|
+
{ id: 'duplicate', label: 'Second', href: '/second' },
|
|
2610
|
+
];
|
|
2611
|
+
|
|
2612
|
+
renderWithProviders(
|
|
2613
|
+
<NavigationMenu
|
|
2614
|
+
items={duplicateItems}
|
|
2615
|
+
onNavigate={mockNavigate}
|
|
2616
|
+
buttonText="Menu"
|
|
2617
|
+
/>
|
|
2618
|
+
);
|
|
2619
|
+
|
|
2620
|
+
await user.click(screen.getByRole('combobox'));
|
|
2621
|
+
await waitFor(() => {
|
|
2622
|
+
// Both items should render (React will use key for rendering)
|
|
2623
|
+
// Use getAllByText since there are duplicates
|
|
2624
|
+
const firstItems = screen.getAllByText('First');
|
|
2625
|
+
const secondItems = screen.getAllByText('Second');
|
|
2626
|
+
expect(firstItems.length).toBeGreaterThan(0);
|
|
2627
|
+
expect(secondItems.length).toBeGreaterThan(0);
|
|
2628
|
+
}, { interval: 10 });
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
it('handles items with very long labels', async () => {
|
|
2632
|
+
const user = userEvent.setup();
|
|
2633
|
+
const longLabelItems: NavigationItem[] = [
|
|
2634
|
+
{ id: 'long', label: 'A'.repeat(1000), href: '/long' },
|
|
2635
|
+
];
|
|
2636
|
+
|
|
2637
|
+
renderWithProviders(
|
|
2638
|
+
<NavigationMenu
|
|
2639
|
+
items={longLabelItems}
|
|
2640
|
+
onNavigate={mockNavigate}
|
|
2641
|
+
buttonText="Menu"
|
|
2642
|
+
/>
|
|
2643
|
+
);
|
|
2644
|
+
|
|
2645
|
+
await user.click(screen.getByRole('combobox'));
|
|
2646
|
+
await waitFor(() => {
|
|
2647
|
+
expect(screen.getByText('A'.repeat(1000))).toBeInTheDocument();
|
|
2648
|
+
}, { interval: 10 });
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
it('handles items with special characters in labels', async () => {
|
|
2652
|
+
const user = userEvent.setup();
|
|
2653
|
+
const specialCharItems: NavigationItem[] = [
|
|
2654
|
+
{ id: 'special', label: 'Item & <Special> "Chars"', href: '/special' },
|
|
2655
|
+
];
|
|
2656
|
+
|
|
2657
|
+
renderWithProviders(
|
|
2658
|
+
<NavigationMenu
|
|
2659
|
+
items={specialCharItems}
|
|
2660
|
+
onNavigate={mockNavigate}
|
|
2661
|
+
buttonText="Menu"
|
|
2662
|
+
/>
|
|
2663
|
+
);
|
|
2664
|
+
|
|
2665
|
+
await user.click(screen.getByRole('combobox'));
|
|
2666
|
+
await waitFor(() => {
|
|
2667
|
+
expect(screen.getByText('Item & <Special> "Chars"')).toBeInTheDocument();
|
|
2668
|
+
}, { interval: 10 });
|
|
2669
|
+
});
|
|
2670
|
+
});
|
|
2671
|
+
|
|
2672
|
+
describe('Hierarchical Mode Edge Cases', () => {
|
|
2673
|
+
it('handles hierarchical item with empty children array', () => {
|
|
2674
|
+
const itemsWithEmptyChildren: NavigationItem[] = [
|
|
2675
|
+
{
|
|
2676
|
+
id: 'parent',
|
|
2677
|
+
label: 'Parent',
|
|
2678
|
+
children: [],
|
|
2679
|
+
},
|
|
2680
|
+
];
|
|
2681
|
+
|
|
2682
|
+
renderWithProviders(
|
|
2683
|
+
<NavigationMenu
|
|
2684
|
+
items={itemsWithEmptyChildren}
|
|
2685
|
+
mode="hierarchical"
|
|
2686
|
+
onNavigate={mockNavigate}
|
|
2687
|
+
/>
|
|
2688
|
+
);
|
|
2689
|
+
|
|
2690
|
+
// Items with empty children array are rendered as buttons
|
|
2691
|
+
// But the component checks hasChildren which is false for empty array
|
|
2692
|
+
// So it might render as a link or not render at all
|
|
2693
|
+
// Let's check that the component renders without crashing
|
|
2694
|
+
expect(screen.getByText('Parent')).toBeInTheDocument();
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
it('handles hierarchical item with only hidden children', async () => {
|
|
2698
|
+
const user = userEvent.setup();
|
|
2699
|
+
// Note: Hidden children are filtered by useNavigationFiltering hook
|
|
2700
|
+
// So by the time items reach the component, hidden children are already removed
|
|
2701
|
+
// This test verifies the component handles items with no visible children
|
|
2702
|
+
const itemsWithHiddenChildren: NavigationItem[] = [
|
|
2703
|
+
{
|
|
2704
|
+
id: 'parent',
|
|
2705
|
+
label: 'Parent',
|
|
2706
|
+
// Children are already filtered out by the hook, so parent has no children
|
|
2707
|
+
children: [],
|
|
2708
|
+
},
|
|
2709
|
+
];
|
|
2710
|
+
|
|
2711
|
+
renderWithProviders(
|
|
2712
|
+
<NavigationMenu
|
|
2713
|
+
items={itemsWithHiddenChildren}
|
|
2714
|
+
mode="hierarchical"
|
|
2715
|
+
onNavigate={mockNavigate}
|
|
2716
|
+
/>
|
|
2717
|
+
);
|
|
2718
|
+
|
|
2719
|
+
// Parent should render (even without children)
|
|
2720
|
+
expect(screen.getByText('Parent')).toBeInTheDocument();
|
|
2721
|
+
|
|
2722
|
+
// Since children array is empty, parent won't be expandable
|
|
2723
|
+
// The component checks hasChildren which is false for empty array
|
|
2724
|
+
const parentElement = screen.getByText('Parent');
|
|
2725
|
+
expect(parentElement).toBeInTheDocument();
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
it('handles hierarchical item where parent has href and children', async () => {
|
|
2729
|
+
const user = userEvent.setup();
|
|
2730
|
+
const itemsWithHrefAndChildren: NavigationItem[] = [
|
|
2731
|
+
{
|
|
2732
|
+
id: 'parent',
|
|
2733
|
+
label: 'Parent',
|
|
2734
|
+
href: '/parent',
|
|
2735
|
+
children: [
|
|
2736
|
+
{ id: 'child', label: 'Child', href: '/child' },
|
|
2737
|
+
],
|
|
2738
|
+
},
|
|
2739
|
+
];
|
|
2740
|
+
|
|
2741
|
+
renderWithProviders(
|
|
2742
|
+
<NavigationMenu
|
|
2743
|
+
items={itemsWithHrefAndChildren}
|
|
2744
|
+
mode="hierarchical"
|
|
2745
|
+
onNavigate={mockNavigate}
|
|
2746
|
+
/>
|
|
2747
|
+
);
|
|
2748
|
+
|
|
2749
|
+
// Parent should be a button (expandable) even though it has href
|
|
2750
|
+
const parent = screen.getByRole('button', { name: /Parent/i });
|
|
2751
|
+
expect(parent).toBeInTheDocument();
|
|
2752
|
+
|
|
2753
|
+
// Click to expand
|
|
2754
|
+
await user.click(parent);
|
|
2755
|
+
await waitFor(() => {
|
|
2756
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
2757
|
+
});
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
it('handles deeply nested items (4+ levels)', async () => {
|
|
2761
|
+
const user = userEvent.setup();
|
|
2762
|
+
const deeplyNestedItems: NavigationItem[] = [
|
|
2763
|
+
{
|
|
2764
|
+
id: 'level1',
|
|
2765
|
+
label: 'Level 1',
|
|
2766
|
+
children: [
|
|
2767
|
+
{
|
|
2768
|
+
id: 'level2',
|
|
2769
|
+
label: 'Level 2',
|
|
2770
|
+
children: [
|
|
2771
|
+
{
|
|
2772
|
+
id: 'level3',
|
|
2773
|
+
label: 'Level 3',
|
|
2774
|
+
children: [
|
|
2775
|
+
{
|
|
2776
|
+
id: 'level4',
|
|
2777
|
+
label: 'Level 4',
|
|
2778
|
+
children: [
|
|
2779
|
+
{ id: 'level5', label: 'Level 5', href: '/level5' },
|
|
2780
|
+
],
|
|
2781
|
+
},
|
|
2782
|
+
],
|
|
2783
|
+
},
|
|
2784
|
+
],
|
|
2785
|
+
},
|
|
2786
|
+
],
|
|
2787
|
+
},
|
|
2788
|
+
];
|
|
2789
|
+
|
|
2790
|
+
renderWithProviders(
|
|
2791
|
+
<NavigationMenu
|
|
2792
|
+
items={deeplyNestedItems}
|
|
2793
|
+
mode="hierarchical"
|
|
2794
|
+
onNavigate={mockNavigate}
|
|
2795
|
+
/>
|
|
2796
|
+
);
|
|
2797
|
+
|
|
2798
|
+
// Expand all levels
|
|
2799
|
+
const level1 = screen.getByRole('button', { name: /Level 1/i });
|
|
2800
|
+
await user.click(level1);
|
|
2801
|
+
|
|
2802
|
+
await waitFor(() => {
|
|
2803
|
+
const level2 = screen.getByRole('button', { name: /Level 2/i });
|
|
2804
|
+
expect(level2).toBeInTheDocument();
|
|
2805
|
+
});
|
|
2806
|
+
|
|
2807
|
+
const level2 = screen.getByRole('button', { name: /Level 2/i });
|
|
2808
|
+
await user.click(level2);
|
|
2809
|
+
|
|
2810
|
+
await waitFor(() => {
|
|
2811
|
+
const level3 = screen.getByRole('button', { name: /Level 3/i });
|
|
2812
|
+
expect(level3).toBeInTheDocument();
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
const level3 = screen.getByRole('button', { name: /Level 3/i });
|
|
2816
|
+
await user.click(level3);
|
|
2817
|
+
|
|
2818
|
+
await waitFor(() => {
|
|
2819
|
+
const level4 = screen.getByRole('button', { name: /Level 4/i });
|
|
2820
|
+
expect(level4).toBeInTheDocument();
|
|
2821
|
+
});
|
|
2822
|
+
|
|
2823
|
+
const level4 = screen.getByRole('button', { name: /Level 4/i });
|
|
2824
|
+
await user.click(level4);
|
|
2825
|
+
|
|
2826
|
+
await waitFor(() => {
|
|
2827
|
+
expect(screen.getByText('Level 5')).toBeInTheDocument();
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
});
|
|
2831
|
+
|
|
2832
|
+
describe('Button Text Variations', () => {
|
|
2833
|
+
it('uses default buttonText when not provided', () => {
|
|
2834
|
+
renderWithProviders(
|
|
2835
|
+
<NavigationMenu
|
|
2836
|
+
items={basicNavItems}
|
|
2837
|
+
onNavigate={mockNavigate}
|
|
2838
|
+
/>
|
|
2839
|
+
);
|
|
2840
|
+
|
|
2841
|
+
expect(screen.getByText('Menu')).toBeInTheDocument();
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
it('uses custom buttonText when provided', () => {
|
|
2845
|
+
renderWithProviders(
|
|
2846
|
+
<NavigationMenu
|
|
2847
|
+
items={basicNavItems}
|
|
2848
|
+
onNavigate={mockNavigate}
|
|
2849
|
+
buttonText="Custom Menu Text"
|
|
2850
|
+
/>
|
|
2851
|
+
);
|
|
2852
|
+
|
|
2853
|
+
expect(screen.getByText('Custom Menu Text')).toBeInTheDocument();
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2856
|
+
it('handles empty buttonText', () => {
|
|
2857
|
+
renderWithProviders(
|
|
2858
|
+
<NavigationMenu
|
|
2859
|
+
items={basicNavItems}
|
|
2860
|
+
onNavigate={mockNavigate}
|
|
2861
|
+
buttonText=""
|
|
2862
|
+
/>
|
|
2863
|
+
);
|
|
2864
|
+
|
|
2865
|
+
const trigger = screen.getByRole('combobox');
|
|
2866
|
+
expect(trigger).toHaveAttribute('aria-label', '');
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2869
|
+
it('handles buttonText with special characters', () => {
|
|
2870
|
+
renderWithProviders(
|
|
2871
|
+
<NavigationMenu
|
|
2872
|
+
items={basicNavItems}
|
|
2873
|
+
onNavigate={mockNavigate}
|
|
2874
|
+
buttonText="Menu & Navigation <Test>"
|
|
2875
|
+
/>
|
|
2876
|
+
);
|
|
2877
|
+
|
|
2878
|
+
expect(screen.getByText('Menu & Navigation <Test>')).toBeInTheDocument();
|
|
2879
|
+
});
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
describe('Select Component Integration', () => {
|
|
2883
|
+
it('handles Select value change correctly', async () => {
|
|
2884
|
+
const user = userEvent.setup();
|
|
2885
|
+
renderWithProviders(
|
|
2886
|
+
<NavigationMenu
|
|
2887
|
+
items={basicNavItems}
|
|
2888
|
+
onNavigate={mockNavigate}
|
|
2889
|
+
buttonText="Menu"
|
|
2890
|
+
/>
|
|
2891
|
+
);
|
|
2892
|
+
|
|
2893
|
+
const trigger = screen.getByRole('combobox');
|
|
2894
|
+
await user.click(trigger);
|
|
2895
|
+
|
|
2896
|
+
await waitFor(() => {
|
|
2897
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
2898
|
+
}, { interval: 10 });
|
|
2899
|
+
|
|
2900
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
2901
|
+
await user.click(homeItem);
|
|
2902
|
+
|
|
2903
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
2904
|
+
expect.objectContaining({
|
|
2905
|
+
id: 'home',
|
|
2906
|
+
label: 'Home',
|
|
2907
|
+
href: '/',
|
|
2908
|
+
})
|
|
2909
|
+
);
|
|
2910
|
+
});
|
|
2911
|
+
|
|
2912
|
+
it('disables SelectItem when item has no href', async () => {
|
|
2913
|
+
const user = userEvent.setup();
|
|
2914
|
+
const itemsWithoutHref: NavigationItem[] = [
|
|
2915
|
+
{ id: 'action', label: 'Action' },
|
|
2916
|
+
];
|
|
2917
|
+
|
|
2918
|
+
renderWithProviders(
|
|
2919
|
+
<NavigationMenu
|
|
2920
|
+
items={itemsWithoutHref}
|
|
2921
|
+
onNavigate={mockNavigate}
|
|
2922
|
+
buttonText="Menu"
|
|
2923
|
+
/>
|
|
2924
|
+
);
|
|
2925
|
+
|
|
2926
|
+
await user.click(screen.getByRole('combobox'));
|
|
2927
|
+
await waitFor(() => {
|
|
2928
|
+
const actionItem = screen.getByText('Action');
|
|
2929
|
+
expect(actionItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
|
|
2930
|
+
}, { interval: 10 });
|
|
2931
|
+
});
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2934
|
+
describe('Keyboard Navigation Edge Cases', () => {
|
|
2935
|
+
it('handles Space key on hierarchical leaf item without href', async () => {
|
|
2936
|
+
const user = userEvent.setup();
|
|
2937
|
+
const leafWithoutHref: NavigationItem[] = [
|
|
2938
|
+
{ id: 'leaf', label: 'Leaf' },
|
|
2939
|
+
];
|
|
2940
|
+
|
|
2941
|
+
renderWithProviders(
|
|
2942
|
+
<NavigationMenu
|
|
2943
|
+
items={leafWithoutHref}
|
|
2944
|
+
mode="hierarchical"
|
|
2945
|
+
onNavigate={mockNavigate}
|
|
2946
|
+
/>
|
|
2947
|
+
);
|
|
2948
|
+
|
|
2949
|
+
const leafControl = screen.getByText('Leaf').closest('button') ?? screen.getByText('Leaf');
|
|
2950
|
+
leafControl.focus();
|
|
2951
|
+
await user.keyboard(' ');
|
|
2952
|
+
|
|
2953
|
+
// Action items without href still call onNavigate so the consumer can handle them
|
|
2954
|
+
expect(mockNavigate).toHaveBeenCalledWith(expect.objectContaining({ id: 'leaf', label: 'Leaf' }));
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
it('handles Escape key on non-expanded hierarchical item', async () => {
|
|
2958
|
+
const user = userEvent.setup();
|
|
2959
|
+
renderWithProviders(
|
|
2960
|
+
<NavigationMenu
|
|
2961
|
+
items={hierarchicalNavItems}
|
|
2962
|
+
mode="hierarchical"
|
|
2963
|
+
onNavigate={mockNavigate}
|
|
2964
|
+
/>
|
|
2965
|
+
);
|
|
2966
|
+
|
|
2967
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
2968
|
+
userManagementButton.focus();
|
|
2969
|
+
|
|
2970
|
+
// Press Escape when not expanded - should not crash
|
|
2971
|
+
await user.keyboard('{Escape}');
|
|
2972
|
+
|
|
2973
|
+
// Should still be collapsed
|
|
2974
|
+
expect(screen.queryByText('All Users')).not.toBeInTheDocument();
|
|
2975
|
+
});
|
|
2976
|
+
|
|
2977
|
+
it('handles Enter key on hierarchical item with children and href', async () => {
|
|
2978
|
+
const user = userEvent.setup();
|
|
2979
|
+
const itemsWithBoth: NavigationItem[] = [
|
|
2980
|
+
{
|
|
2981
|
+
id: 'parent',
|
|
2982
|
+
label: 'Parent',
|
|
2983
|
+
href: '/parent',
|
|
2984
|
+
children: [
|
|
2985
|
+
{ id: 'child', label: 'Child', href: '/child' },
|
|
2986
|
+
],
|
|
2987
|
+
},
|
|
2988
|
+
];
|
|
2989
|
+
|
|
2990
|
+
renderWithProviders(
|
|
2991
|
+
<NavigationMenu
|
|
2992
|
+
items={itemsWithBoth}
|
|
2993
|
+
mode="hierarchical"
|
|
2994
|
+
onNavigate={mockNavigate}
|
|
2995
|
+
/>
|
|
2996
|
+
);
|
|
2997
|
+
|
|
2998
|
+
const parentButton = screen.getByRole('button', { name: /Parent/i });
|
|
2999
|
+
parentButton.focus();
|
|
3000
|
+
await user.keyboard('{Enter}');
|
|
3001
|
+
|
|
3002
|
+
// Should expand (not navigate) since it has children
|
|
3003
|
+
await waitFor(() => {
|
|
3004
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
3005
|
+
});
|
|
3006
|
+
});
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
describe('Ref Forwarding', () => {
|
|
3010
|
+
it('forwards ref correctly in hierarchical mode', () => {
|
|
3011
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
3012
|
+
renderWithProviders(
|
|
3013
|
+
<NavigationMenu
|
|
3014
|
+
ref={ref}
|
|
3015
|
+
items={basicNavItems}
|
|
3016
|
+
mode="hierarchical"
|
|
3017
|
+
onNavigate={mockNavigate}
|
|
3018
|
+
/>
|
|
3019
|
+
);
|
|
3020
|
+
|
|
3021
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
3022
|
+
expect(ref.current?.tagName).toBe('NAV');
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
it('ref is null when component unmounts', () => {
|
|
3026
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
3027
|
+
const { unmount } = renderWithProviders(
|
|
3028
|
+
<NavigationMenu
|
|
3029
|
+
ref={ref}
|
|
3030
|
+
items={basicNavItems}
|
|
3031
|
+
mode="hierarchical"
|
|
3032
|
+
onNavigate={mockNavigate}
|
|
3033
|
+
/>
|
|
3034
|
+
);
|
|
3035
|
+
|
|
3036
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
3037
|
+
unmount();
|
|
3038
|
+
expect(ref.current).toBeNull();
|
|
3039
|
+
});
|
|
3040
|
+
|
|
3041
|
+
it('handles ref callback function', () => {
|
|
3042
|
+
let refElement: HTMLDivElement | null = null;
|
|
3043
|
+
const refCallback = (node: HTMLDivElement | null) => {
|
|
3044
|
+
refElement = node;
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3047
|
+
renderWithProviders(
|
|
3048
|
+
<NavigationMenu
|
|
3049
|
+
ref={refCallback}
|
|
3050
|
+
items={basicNavItems}
|
|
3051
|
+
mode="hierarchical"
|
|
3052
|
+
onNavigate={mockNavigate}
|
|
3053
|
+
/>
|
|
3054
|
+
);
|
|
3055
|
+
|
|
3056
|
+
expect(refElement).toBeInstanceOf(HTMLElement);
|
|
3057
|
+
expect(refElement?.tagName).toBe('NAV');
|
|
3058
|
+
});
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
describe('Select Component Integration Details', () => {
|
|
3062
|
+
it('handles Select value change with item that has no href', async () => {
|
|
3063
|
+
const user = userEvent.setup();
|
|
3064
|
+
const itemsWithoutHref: NavigationItem[] = [
|
|
3065
|
+
{ id: 'action', label: 'Action Item' },
|
|
3066
|
+
{ id: 'with-href', label: 'With Href', href: '/with-href' },
|
|
3067
|
+
];
|
|
3068
|
+
|
|
3069
|
+
renderWithProviders(
|
|
3070
|
+
<NavigationMenu
|
|
3071
|
+
items={itemsWithoutHref}
|
|
3072
|
+
onNavigate={mockNavigate}
|
|
3073
|
+
buttonText="Menu"
|
|
3074
|
+
/>
|
|
3075
|
+
);
|
|
3076
|
+
|
|
3077
|
+
const trigger = screen.getByRole('combobox');
|
|
3078
|
+
await user.click(trigger);
|
|
3079
|
+
|
|
3080
|
+
await waitFor(() => {
|
|
3081
|
+
expect(screen.getByText('Action Item')).toBeInTheDocument();
|
|
3082
|
+
expect(screen.getByText('With Href')).toBeInTheDocument();
|
|
3083
|
+
}, { interval: 10 });
|
|
3084
|
+
|
|
3085
|
+
// Items without href should be disabled
|
|
3086
|
+
const actionItem = screen.getByText('Action Item');
|
|
3087
|
+
expect(actionItem.closest('[role="option"]')).toHaveAttribute('data-disabled', 'true');
|
|
3088
|
+
});
|
|
3089
|
+
|
|
3090
|
+
it('handles Select with empty items gracefully', async () => {
|
|
3091
|
+
const user = userEvent.setup();
|
|
3092
|
+
renderWithProviders(
|
|
3093
|
+
<NavigationMenu
|
|
3094
|
+
items={[]}
|
|
3095
|
+
onNavigate={mockNavigate}
|
|
3096
|
+
buttonText="Empty Menu"
|
|
3097
|
+
/>
|
|
3098
|
+
);
|
|
3099
|
+
|
|
3100
|
+
const trigger = screen.getByRole('combobox');
|
|
3101
|
+
await user.click(trigger);
|
|
3102
|
+
|
|
3103
|
+
// Should not crash with empty items
|
|
3104
|
+
expect(trigger).toBeInTheDocument();
|
|
3105
|
+
});
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
describe('NavigationMenu Display Name', () => {
|
|
3109
|
+
it('has correct displayName', () => {
|
|
3110
|
+
expect(NavigationMenu.displayName).toBe('NavigationMenu');
|
|
3111
|
+
});
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
describe('Current Path Edge Cases', () => {
|
|
3115
|
+
it('handles currentPath with trailing slash', () => {
|
|
3116
|
+
renderWithProviders(
|
|
3117
|
+
<NavigationMenu
|
|
3118
|
+
items={basicNavItems}
|
|
3119
|
+
mode="hierarchical"
|
|
3120
|
+
onNavigate={mockNavigate}
|
|
3121
|
+
currentPath="/dashboard/"
|
|
3122
|
+
/>
|
|
3123
|
+
);
|
|
3124
|
+
|
|
3125
|
+
const dashboardLink = screen.getByText('Dashboard');
|
|
3126
|
+
// Component checks exact match, so /dashboard/ won't match /dashboard
|
|
3127
|
+
// But component should still render without errors
|
|
3128
|
+
expect(dashboardLink).toBeInTheDocument();
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
it('handles currentPath with query parameters', () => {
|
|
3132
|
+
renderWithProviders(
|
|
3133
|
+
<NavigationMenu
|
|
3134
|
+
items={basicNavItems}
|
|
3135
|
+
mode="hierarchical"
|
|
3136
|
+
onNavigate={mockNavigate}
|
|
3137
|
+
currentPath="/dashboard?tab=settings"
|
|
3138
|
+
/>
|
|
3139
|
+
);
|
|
3140
|
+
|
|
3141
|
+
const dashboardLink = screen.getByText('Dashboard');
|
|
3142
|
+
// Component checks exact match, so query params won't match
|
|
3143
|
+
// But component should still render without errors
|
|
3144
|
+
expect(dashboardLink).toBeInTheDocument();
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
it('handles currentPath with hash', () => {
|
|
3148
|
+
renderWithProviders(
|
|
3149
|
+
<NavigationMenu
|
|
3150
|
+
items={basicNavItems}
|
|
3151
|
+
mode="hierarchical"
|
|
3152
|
+
onNavigate={mockNavigate}
|
|
3153
|
+
currentPath="/dashboard#section"
|
|
3154
|
+
/>
|
|
3155
|
+
);
|
|
3156
|
+
|
|
3157
|
+
const dashboardLink = screen.getByText('Dashboard');
|
|
3158
|
+
expect(dashboardLink).toBeInTheDocument();
|
|
3159
|
+
});
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
describe('Hierarchical Mode Rendering Details', () => {
|
|
3163
|
+
it('renders hierarchical items with proper ARIA structure', () => {
|
|
3164
|
+
renderWithProviders(
|
|
3165
|
+
<NavigationMenu
|
|
3166
|
+
items={hierarchicalNavItems}
|
|
3167
|
+
mode="hierarchical"
|
|
3168
|
+
onNavigate={mockNavigate}
|
|
3169
|
+
/>
|
|
3170
|
+
);
|
|
3171
|
+
|
|
3172
|
+
const menubar = screen.getByRole('menubar');
|
|
3173
|
+
expect(menubar).toBeInTheDocument();
|
|
3174
|
+
|
|
3175
|
+
// Check that items are rendered with proper roles
|
|
3176
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
3177
|
+
expect(userManagementButton).toHaveAttribute('aria-expanded', 'false');
|
|
3178
|
+
expect(userManagementButton).toHaveAttribute('aria-controls');
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
it('renders hierarchical items with proper submenu structure', async () => {
|
|
3182
|
+
const user = userEvent.setup();
|
|
3183
|
+
renderWithProviders(
|
|
3184
|
+
<NavigationMenu
|
|
3185
|
+
items={hierarchicalNavItems}
|
|
3186
|
+
mode="hierarchical"
|
|
3187
|
+
onNavigate={mockNavigate}
|
|
3188
|
+
/>
|
|
3189
|
+
);
|
|
3190
|
+
|
|
3191
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
3192
|
+
await user.click(userManagementButton);
|
|
3193
|
+
|
|
3194
|
+
await waitFor(() => {
|
|
3195
|
+
const submenu = screen.getByRole('menu', { name: /User Management submenu/i });
|
|
3196
|
+
expect(submenu).toBeInTheDocument();
|
|
3197
|
+
expect(submenu).toHaveAttribute('id');
|
|
3198
|
+
}, { interval: 10 });
|
|
3199
|
+
});
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
describe('Permission Checking Edge Cases', () => {
|
|
3203
|
+
it('handles permission check when permissionMap is empty but item is visible', async () => {
|
|
3204
|
+
const user = userEvent.setup();
|
|
3205
|
+
|
|
3206
|
+
mockUsePermissions.mockReturnValue({
|
|
3207
|
+
permissions: {} as any,
|
|
3208
|
+
isLoading: false,
|
|
3209
|
+
error: null,
|
|
3210
|
+
hasPermission: vi.fn(() => false),
|
|
3211
|
+
hasAnyPermission: vi.fn(() => false),
|
|
3212
|
+
hasAllPermissions: vi.fn(() => false),
|
|
3213
|
+
refetch: vi.fn(),
|
|
3214
|
+
});
|
|
3215
|
+
|
|
3216
|
+
renderWithProviders(
|
|
3217
|
+
<NavigationMenu
|
|
3218
|
+
items={basicNavItems}
|
|
3219
|
+
onNavigate={mockNavigate}
|
|
3220
|
+
buttonText="Menu"
|
|
3221
|
+
itemsPreFiltered={true}
|
|
3222
|
+
/>
|
|
3223
|
+
);
|
|
3224
|
+
|
|
3225
|
+
await user.click(screen.getByRole('combobox'));
|
|
3226
|
+
await waitFor(() => {
|
|
3227
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3228
|
+
}, { interval: 10 });
|
|
3229
|
+
|
|
3230
|
+
// Click on item - should navigate even with empty permission map if item is visible
|
|
3231
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
3232
|
+
await user.click(homeItem);
|
|
3233
|
+
|
|
3234
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
3235
|
+
expect.objectContaining({
|
|
3236
|
+
id: 'home',
|
|
3237
|
+
href: '/',
|
|
3238
|
+
})
|
|
3239
|
+
);
|
|
3240
|
+
});
|
|
3241
|
+
|
|
3242
|
+
it('handles role check with case-insensitive matching', async () => {
|
|
3243
|
+
const user = userEvent.setup();
|
|
3244
|
+
|
|
3245
|
+
mockUseRBAC.mockReturnValue({
|
|
3246
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3247
|
+
globalRole: null,
|
|
3248
|
+
organisationRole: 'org_admin' as any,
|
|
3249
|
+
eventAppRole: null,
|
|
3250
|
+
hasPermission: vi.fn(),
|
|
3251
|
+
hasGlobalPermission: vi.fn(),
|
|
3252
|
+
isSuperAdmin: false,
|
|
3253
|
+
isOrgAdmin: true,
|
|
3254
|
+
isEventAdmin: false,
|
|
3255
|
+
canManageOrganisation: true,
|
|
3256
|
+
canManageEvent: false,
|
|
3257
|
+
isLoading: false,
|
|
3258
|
+
error: null,
|
|
3259
|
+
});
|
|
3260
|
+
|
|
3261
|
+
const itemsWithCaseVariations: NavigationItem[] = [
|
|
3262
|
+
{ id: 'admin1', label: 'Admin 1', href: '/admin1', roles: ['admin'] },
|
|
3263
|
+
{ id: 'admin2', label: 'Admin 2', href: '/admin2', roles: ['ADMIN'] },
|
|
3264
|
+
{ id: 'admin3', label: 'Admin 3', href: '/admin3', roles: ['Admin'] },
|
|
3265
|
+
{ id: 'super1', label: 'Super 1', href: '/super1', roles: ['super_admin'] },
|
|
3266
|
+
{ id: 'super2', label: 'Super 2', href: '/super2', roles: ['SUPER_ADMIN'] },
|
|
3267
|
+
{ id: 'super3', label: 'Super 3', href: '/super3', roles: ['Super Admin'] },
|
|
3268
|
+
];
|
|
3269
|
+
|
|
3270
|
+
mockUsePermissions.mockReturnValue({
|
|
3271
|
+
permissions: {
|
|
3272
|
+
'read:page.admin1': true,
|
|
3273
|
+
'read:page.admin2': true,
|
|
3274
|
+
'read:page.admin3': true,
|
|
3275
|
+
'read:page.super1': true,
|
|
3276
|
+
'read:page.super2': true,
|
|
3277
|
+
'read:page.super3': true,
|
|
3278
|
+
} as any,
|
|
3279
|
+
isLoading: false,
|
|
3280
|
+
error: null,
|
|
3281
|
+
hasPermission: vi.fn(() => true),
|
|
3282
|
+
hasAnyPermission: vi.fn(() => true),
|
|
3283
|
+
hasAllPermissions: vi.fn(() => true),
|
|
3284
|
+
refetch: vi.fn(),
|
|
3285
|
+
});
|
|
3286
|
+
|
|
3287
|
+
renderWithProviders(
|
|
3288
|
+
<NavigationMenu
|
|
3289
|
+
items={itemsWithCaseVariations}
|
|
3290
|
+
onNavigate={mockNavigate}
|
|
3291
|
+
buttonText="Menu"
|
|
3292
|
+
/>
|
|
3293
|
+
);
|
|
3294
|
+
|
|
3295
|
+
await user.click(screen.getByRole('combobox'));
|
|
3296
|
+
await waitFor(() => {
|
|
3297
|
+
// All admin items should be visible (case-insensitive matching)
|
|
3298
|
+
expect(screen.getByText('Admin 1')).toBeInTheDocument();
|
|
3299
|
+
expect(screen.getByText('Admin 2')).toBeInTheDocument();
|
|
3300
|
+
expect(screen.getByText('Admin 3')).toBeInTheDocument();
|
|
3301
|
+
}, { interval: 10 });
|
|
3302
|
+
});
|
|
3303
|
+
});
|
|
3304
|
+
|
|
3305
|
+
describe('Accessibility - Extended', () => {
|
|
3306
|
+
it('has proper ARIA attributes for hierarchical items with children', async () => {
|
|
3307
|
+
const user = userEvent.setup();
|
|
3308
|
+
renderWithProviders(
|
|
3309
|
+
<NavigationMenu
|
|
3310
|
+
items={hierarchicalNavItems}
|
|
3311
|
+
mode="hierarchical"
|
|
3312
|
+
onNavigate={mockNavigate}
|
|
3313
|
+
/>
|
|
3314
|
+
);
|
|
3315
|
+
|
|
3316
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
3317
|
+
expect(userManagementButton).toHaveAttribute('aria-expanded', 'false');
|
|
3318
|
+
expect(userManagementButton).toHaveAttribute('aria-controls');
|
|
3319
|
+
|
|
3320
|
+
await user.click(userManagementButton);
|
|
3321
|
+
await waitFor(() => {
|
|
3322
|
+
expect(userManagementButton).toHaveAttribute('aria-expanded', 'true');
|
|
3323
|
+
}, { interval: 10 });
|
|
3324
|
+
});
|
|
3325
|
+
|
|
3326
|
+
it('has proper ARIA attributes for hierarchical leaf items', async () => {
|
|
3327
|
+
const user = userEvent.setup();
|
|
3328
|
+
renderWithProviders(
|
|
3329
|
+
<NavigationMenu
|
|
3330
|
+
items={hierarchicalNavItems}
|
|
3331
|
+
mode="hierarchical"
|
|
3332
|
+
onNavigate={mockNavigate}
|
|
3333
|
+
currentPath="/users"
|
|
3334
|
+
/>
|
|
3335
|
+
);
|
|
3336
|
+
|
|
3337
|
+
// Expand the parent to see the leaf item
|
|
3338
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
3339
|
+
await user.click(userManagementButton);
|
|
3340
|
+
|
|
3341
|
+
await waitFor(() => {
|
|
3342
|
+
const allUsersLink = screen.getByText('All Users');
|
|
3343
|
+
expect(allUsersLink.closest('a')).toHaveAttribute('role', 'menuitem');
|
|
3344
|
+
expect(allUsersLink.closest('a')).toHaveAttribute('aria-current', 'page');
|
|
3345
|
+
}, { interval: 10 });
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
it('has proper ARIA attributes for active parent when child is active', () => {
|
|
3349
|
+
renderWithProviders(
|
|
3350
|
+
<NavigationMenu
|
|
3351
|
+
items={hierarchicalNavItems}
|
|
3352
|
+
mode="hierarchical"
|
|
3353
|
+
onNavigate={mockNavigate}
|
|
3354
|
+
currentPath="/users"
|
|
3355
|
+
/>
|
|
3356
|
+
);
|
|
3357
|
+
|
|
3358
|
+
const userManagementButton = screen.getByRole('button', { name: /User Management/i });
|
|
3359
|
+
expect(userManagementButton).toHaveAttribute('aria-current', 'page');
|
|
3360
|
+
});
|
|
3361
|
+
});
|
|
3362
|
+
|
|
3363
|
+
describe('Error Handling - Extended', () => {
|
|
3364
|
+
it('handles items with null children', () => {
|
|
3365
|
+
const itemsWithNullChildren: NavigationItem[] = [
|
|
3366
|
+
{
|
|
3367
|
+
id: 'parent',
|
|
3368
|
+
label: 'Parent',
|
|
3369
|
+
children: null as any,
|
|
3370
|
+
},
|
|
3371
|
+
];
|
|
3372
|
+
|
|
3373
|
+
renderWithProviders(
|
|
3374
|
+
<NavigationMenu
|
|
3375
|
+
items={itemsWithNullChildren}
|
|
3376
|
+
mode="hierarchical"
|
|
3377
|
+
onNavigate={mockNavigate}
|
|
3378
|
+
/>
|
|
3379
|
+
);
|
|
3380
|
+
|
|
3381
|
+
expect(screen.getByText('Parent')).toBeInTheDocument();
|
|
3382
|
+
});
|
|
3383
|
+
|
|
3384
|
+
it('handles items with undefined children', () => {
|
|
3385
|
+
const itemsWithUndefinedChildren: NavigationItem[] = [
|
|
3386
|
+
{
|
|
3387
|
+
id: 'parent',
|
|
3388
|
+
label: 'Parent',
|
|
3389
|
+
children: undefined as any,
|
|
3390
|
+
},
|
|
3391
|
+
];
|
|
3392
|
+
|
|
3393
|
+
renderWithProviders(
|
|
3394
|
+
<NavigationMenu
|
|
3395
|
+
items={itemsWithUndefinedChildren}
|
|
3396
|
+
mode="hierarchical"
|
|
3397
|
+
onNavigate={mockNavigate}
|
|
3398
|
+
/>
|
|
3399
|
+
);
|
|
3400
|
+
|
|
3401
|
+
expect(screen.getByText('Parent')).toBeInTheDocument();
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
it('handles items with mixed valid and invalid children', async () => {
|
|
3405
|
+
const user = userEvent.setup();
|
|
3406
|
+
// Filter out null/undefined before passing to component
|
|
3407
|
+
// The component expects valid NavigationItem[] or undefined
|
|
3408
|
+
const itemsWithMixedChildren: NavigationItem[] = [
|
|
3409
|
+
{
|
|
3410
|
+
id: 'parent',
|
|
3411
|
+
label: 'Parent',
|
|
3412
|
+
children: [
|
|
3413
|
+
{ id: 'valid', label: 'Valid', href: '/valid' },
|
|
3414
|
+
{ id: 'another-valid', label: 'Another Valid', href: '/another-valid' },
|
|
3415
|
+
].filter((item): item is NavigationItem => item !== null && item !== undefined),
|
|
3416
|
+
},
|
|
3417
|
+
];
|
|
3418
|
+
|
|
3419
|
+
renderWithProviders(
|
|
3420
|
+
<NavigationMenu
|
|
3421
|
+
items={itemsWithMixedChildren}
|
|
3422
|
+
mode="hierarchical"
|
|
3423
|
+
onNavigate={mockNavigate}
|
|
3424
|
+
/>
|
|
3425
|
+
);
|
|
3426
|
+
|
|
3427
|
+
const parentButton = screen.getByRole('button', { name: /Parent/i });
|
|
3428
|
+
await user.click(parentButton);
|
|
3429
|
+
|
|
3430
|
+
await waitFor(() => {
|
|
3431
|
+
expect(screen.getByText('Valid')).toBeInTheDocument();
|
|
3432
|
+
expect(screen.getByText('Another Valid')).toBeInTheDocument();
|
|
3433
|
+
}, { interval: 10 });
|
|
3434
|
+
});
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
describe('Integration - Extended', () => {
|
|
3438
|
+
it('works with React Router Link components', async () => {
|
|
3439
|
+
const user = userEvent.setup();
|
|
3440
|
+
const itemsWithRouter: NavigationItem[] = [
|
|
3441
|
+
{ id: 'home', label: 'Home', href: '/home' },
|
|
3442
|
+
];
|
|
3443
|
+
|
|
3444
|
+
renderWithProviders(
|
|
3445
|
+
<NavigationMenu
|
|
3446
|
+
items={itemsWithRouter}
|
|
3447
|
+
onNavigate={(item) => {
|
|
3448
|
+
// Simulate React Router navigation
|
|
3449
|
+
mockNavigate(item);
|
|
3450
|
+
}}
|
|
3451
|
+
buttonText="Menu"
|
|
3452
|
+
/>
|
|
3453
|
+
);
|
|
3454
|
+
|
|
3455
|
+
await user.click(screen.getByRole('combobox'));
|
|
3456
|
+
await waitFor(() => {
|
|
3457
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3458
|
+
}, { interval: 10 });
|
|
3459
|
+
|
|
3460
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
3461
|
+
await user.click(homeItem);
|
|
3462
|
+
|
|
3463
|
+
expect(mockNavigate).toHaveBeenCalledWith(
|
|
3464
|
+
expect.objectContaining({
|
|
3465
|
+
id: 'home',
|
|
3466
|
+
href: '/home',
|
|
3467
|
+
})
|
|
3468
|
+
);
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
it('handles rapid item changes gracefully', async () => {
|
|
3472
|
+
const user = userEvent.setup();
|
|
3473
|
+
const { rerender } = renderWithProviders(
|
|
3474
|
+
<NavigationMenu
|
|
3475
|
+
items={basicNavItems}
|
|
3476
|
+
onNavigate={mockNavigate}
|
|
3477
|
+
buttonText="Menu"
|
|
3478
|
+
/>
|
|
3479
|
+
);
|
|
3480
|
+
|
|
3481
|
+
await user.click(screen.getByRole('combobox'));
|
|
3482
|
+
await waitFor(() => {
|
|
3483
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3484
|
+
}, { interval: 10 });
|
|
3485
|
+
|
|
3486
|
+
// Rapidly change items
|
|
3487
|
+
const newItems: NavigationItem[] = [
|
|
3488
|
+
{ id: 'new1', label: 'New 1', href: '/new1' },
|
|
3489
|
+
{ id: 'new2', label: 'New 2', href: '/new2' },
|
|
3490
|
+
];
|
|
3491
|
+
|
|
3492
|
+
rerender(
|
|
3493
|
+
<NavigationMenu
|
|
3494
|
+
items={newItems}
|
|
3495
|
+
onNavigate={mockNavigate}
|
|
3496
|
+
buttonText="Menu"
|
|
3497
|
+
/>
|
|
3498
|
+
);
|
|
3499
|
+
|
|
3500
|
+
// Component should handle rapid changes without crashing
|
|
3501
|
+
await waitFor(() => {
|
|
3502
|
+
const combobox = screen.getByRole('combobox');
|
|
3503
|
+
expect(combobox).toBeInTheDocument();
|
|
3504
|
+
});
|
|
3505
|
+
});
|
|
3506
|
+
});
|
|
3507
|
+
|
|
3508
|
+
describe('handleItemClick - Uncovered Branches', () => {
|
|
3509
|
+
it('handles navigation when authContext is null and onNavigate is not provided', async () => {
|
|
3510
|
+
const user = userEvent.setup();
|
|
3511
|
+
mockUseUnifiedAuthFn.mockImplementationOnce(() => null as any);
|
|
3512
|
+
|
|
3513
|
+
// Mock window.location.href
|
|
3514
|
+
delete (window as any).location;
|
|
3515
|
+
window.location = { href: '' } as any;
|
|
3516
|
+
|
|
3517
|
+
renderWithProviders(
|
|
3518
|
+
<NavigationMenu
|
|
3519
|
+
items={basicNavItems}
|
|
3520
|
+
buttonText="Menu"
|
|
3521
|
+
itemsPreFiltered={true}
|
|
3522
|
+
/>
|
|
3523
|
+
);
|
|
3524
|
+
|
|
3525
|
+
await user.click(screen.getByRole('combobox'));
|
|
3526
|
+
await waitFor(() => {
|
|
3527
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3528
|
+
}, { interval: 10 });
|
|
3529
|
+
|
|
3530
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
3531
|
+
await user.click(homeItem);
|
|
3532
|
+
|
|
3533
|
+
expect(window.location.href).toBe('/');
|
|
3534
|
+
});
|
|
3535
|
+
|
|
3536
|
+
it('blocks navigation when item is not in filteredItems', async () => {
|
|
3537
|
+
const user = userEvent.setup();
|
|
3538
|
+
|
|
3539
|
+
// Mock useNavigationFiltering to return empty filteredItems
|
|
3540
|
+
mockUseNavigationFiltering.mockReturnValueOnce({
|
|
3541
|
+
authContext: mockAuthContext,
|
|
3542
|
+
rbacContext: {
|
|
3543
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3544
|
+
globalRole: null,
|
|
3545
|
+
organisationRole: null,
|
|
3546
|
+
eventAppRole: null,
|
|
3547
|
+
hasPermission: vi.fn(),
|
|
3548
|
+
hasGlobalPermission: vi.fn(),
|
|
3549
|
+
isSuperAdmin: false,
|
|
3550
|
+
isOrgAdmin: false,
|
|
3551
|
+
isEventAdmin: false,
|
|
3552
|
+
canManageOrganisation: false,
|
|
3553
|
+
canManageEvent: false,
|
|
3554
|
+
isLoading: false,
|
|
3555
|
+
error: null,
|
|
3556
|
+
},
|
|
3557
|
+
filteredItems: [], // Empty - item won't be visible
|
|
3558
|
+
permissionMap: {} as any,
|
|
3559
|
+
hasAnyPermission: null,
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
renderWithProviders(
|
|
3563
|
+
<NavigationMenu
|
|
3564
|
+
items={basicNavItems}
|
|
3565
|
+
onNavigate={mockNavigate}
|
|
3566
|
+
buttonText="Menu"
|
|
3567
|
+
/>
|
|
3568
|
+
);
|
|
3569
|
+
|
|
3570
|
+
await user.click(screen.getByRole('combobox'));
|
|
3571
|
+
|
|
3572
|
+
// Item should not be visible if filtered out
|
|
3573
|
+
await waitFor(() => {
|
|
3574
|
+
expect(screen.queryByText('Home')).not.toBeInTheDocument();
|
|
3575
|
+
}, { interval: 10 });
|
|
3576
|
+
});
|
|
3577
|
+
|
|
3578
|
+
it('handles permission check when permissionMap is empty but item is visible', async () => {
|
|
3579
|
+
const user = userEvent.setup();
|
|
3580
|
+
|
|
3581
|
+
mockUsePermissions.mockReturnValue({
|
|
3582
|
+
permissions: {} as any,
|
|
3583
|
+
isLoading: false,
|
|
3584
|
+
error: null,
|
|
3585
|
+
hasPermission: vi.fn(() => false),
|
|
3586
|
+
hasAnyPermission: vi.fn(() => false),
|
|
3587
|
+
hasAllPermissions: vi.fn(() => false),
|
|
3588
|
+
refetch: vi.fn(),
|
|
3589
|
+
});
|
|
3590
|
+
|
|
3591
|
+
// Item is in filteredItems but permissionMap is empty
|
|
3592
|
+
mockUseNavigationFiltering.mockReturnValueOnce({
|
|
3593
|
+
authContext: mockAuthContext,
|
|
3594
|
+
rbacContext: {
|
|
3595
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3596
|
+
globalRole: null,
|
|
3597
|
+
organisationRole: null,
|
|
3598
|
+
eventAppRole: null,
|
|
3599
|
+
hasPermission: vi.fn(),
|
|
3600
|
+
hasGlobalPermission: vi.fn(),
|
|
3601
|
+
isSuperAdmin: false,
|
|
3602
|
+
isOrgAdmin: false,
|
|
3603
|
+
isEventAdmin: false,
|
|
3604
|
+
canManageOrganisation: false,
|
|
3605
|
+
canManageEvent: false,
|
|
3606
|
+
isLoading: false,
|
|
3607
|
+
error: null,
|
|
3608
|
+
},
|
|
3609
|
+
filteredItems: basicNavItems, // Item is visible
|
|
3610
|
+
permissionMap: {}, // Empty permission map
|
|
3611
|
+
hasAnyPermission: null,
|
|
3612
|
+
});
|
|
3613
|
+
|
|
3614
|
+
renderWithProviders(
|
|
3615
|
+
<NavigationMenu
|
|
3616
|
+
items={basicNavItems}
|
|
3617
|
+
onNavigate={mockNavigate}
|
|
3618
|
+
buttonText="Menu"
|
|
3619
|
+
itemsPreFiltered={true}
|
|
3620
|
+
/>
|
|
3621
|
+
);
|
|
3622
|
+
|
|
3623
|
+
await user.click(screen.getByRole('combobox'));
|
|
3624
|
+
await waitFor(() => {
|
|
3625
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3626
|
+
}, { interval: 10 });
|
|
3627
|
+
|
|
3628
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
3629
|
+
await user.click(homeItem);
|
|
3630
|
+
|
|
3631
|
+
// Should navigate even with empty permission map if item is visible
|
|
3632
|
+
expect(mockNavigate).toHaveBeenCalled();
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
it('handles role check with non-standard role strings', async () => {
|
|
3636
|
+
const user = userEvent.setup();
|
|
3637
|
+
|
|
3638
|
+
const itemsWithCustomRole: NavigationItem[] = [
|
|
3639
|
+
{ id: 'custom', label: 'Custom', href: '/custom', roles: ['custom_role'] },
|
|
3640
|
+
];
|
|
3641
|
+
|
|
3642
|
+
mockUseNavigationFiltering.mockImplementation((options: { items?: NavigationItem[] }) => ({
|
|
3643
|
+
authContext: mockAuthContext,
|
|
3644
|
+
rbacContext: {
|
|
3645
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3646
|
+
globalRole: null,
|
|
3647
|
+
organisationRole: 'custom_role',
|
|
3648
|
+
eventAppRole: null,
|
|
3649
|
+
hasPermission: vi.fn(),
|
|
3650
|
+
hasGlobalPermission: vi.fn(),
|
|
3651
|
+
isSuperAdmin: false,
|
|
3652
|
+
isOrgAdmin: false,
|
|
3653
|
+
isEventAdmin: false,
|
|
3654
|
+
canManageOrganisation: false,
|
|
3655
|
+
canManageEvent: false,
|
|
3656
|
+
isLoading: false,
|
|
3657
|
+
error: null,
|
|
3658
|
+
},
|
|
3659
|
+
filteredItems: (options?.items ?? []).filter((item: NavigationItem) => !item.meta?.hidden),
|
|
3660
|
+
permissionMap: { 'read:page.custom': true } as Record<string, boolean>,
|
|
3661
|
+
hasAnyPermission: vi.fn(() => true),
|
|
3662
|
+
}));
|
|
3663
|
+
|
|
3664
|
+
renderWithProviders(
|
|
3665
|
+
<NavigationMenu
|
|
3666
|
+
items={itemsWithCustomRole}
|
|
3667
|
+
onNavigate={mockNavigate}
|
|
3668
|
+
buttonText="Menu"
|
|
3669
|
+
/>
|
|
3670
|
+
);
|
|
3671
|
+
|
|
3672
|
+
await user.click(screen.getByRole('combobox'));
|
|
3673
|
+
await waitFor(() => {
|
|
3674
|
+
expect(screen.getByText('Custom')).toBeInTheDocument();
|
|
3675
|
+
}, { interval: 10 });
|
|
3676
|
+
|
|
3677
|
+
const customItem = screen.getByRole('option', { name: 'Custom' });
|
|
3678
|
+
await user.click(customItem);
|
|
3679
|
+
|
|
3680
|
+
expect(mockNavigate).toHaveBeenCalled();
|
|
3681
|
+
});
|
|
3682
|
+
|
|
3683
|
+
it('handles accessLevel permission check', async () => {
|
|
3684
|
+
const user = userEvent.setup();
|
|
3685
|
+
|
|
3686
|
+
const itemsWithAccessLevel: NavigationItem[] = [
|
|
3687
|
+
{ id: 'admin-only', label: 'Admin Only', href: '/admin-only', accessLevel: 'admin' },
|
|
3688
|
+
];
|
|
3689
|
+
|
|
3690
|
+
mockUseRBAC.mockReturnValue({
|
|
3691
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3692
|
+
globalRole: null,
|
|
3693
|
+
organisationRole: null,
|
|
3694
|
+
eventAppRole: 'planner' as any, // Lower than admin
|
|
3695
|
+
hasPermission: vi.fn(),
|
|
3696
|
+
hasGlobalPermission: vi.fn(),
|
|
3697
|
+
isSuperAdmin: false,
|
|
3698
|
+
isOrgAdmin: false,
|
|
3699
|
+
isEventAdmin: false,
|
|
3700
|
+
canManageOrganisation: false,
|
|
3701
|
+
canManageEvent: false,
|
|
3702
|
+
isLoading: false,
|
|
3703
|
+
error: null,
|
|
3704
|
+
});
|
|
3705
|
+
|
|
3706
|
+
mockUsePermissions.mockReturnValue({
|
|
3707
|
+
permissions: {
|
|
3708
|
+
'read:page.admin-only': true,
|
|
3709
|
+
} as any,
|
|
3710
|
+
isLoading: false,
|
|
3711
|
+
error: null,
|
|
3712
|
+
hasPermission: vi.fn(() => true),
|
|
3713
|
+
hasAnyPermission: vi.fn(() => true),
|
|
3714
|
+
hasAllPermissions: vi.fn(() => true),
|
|
3715
|
+
refetch: vi.fn(),
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
renderWithProviders(
|
|
3719
|
+
<NavigationMenu
|
|
3720
|
+
items={itemsWithAccessLevel}
|
|
3721
|
+
onNavigate={mockNavigate}
|
|
3722
|
+
buttonText="Menu"
|
|
3723
|
+
/>
|
|
3724
|
+
);
|
|
3725
|
+
|
|
3726
|
+
await user.click(screen.getByRole('combobox'));
|
|
3727
|
+
await waitFor(() => {
|
|
3728
|
+
// Item should be filtered out if access level is insufficient
|
|
3729
|
+
const item = screen.queryByText('Admin Only');
|
|
3730
|
+
// May or may not be visible depending on filtering
|
|
3731
|
+
expect(item !== null || item === null).toBe(true);
|
|
3732
|
+
}, { interval: 10 });
|
|
3733
|
+
});
|
|
3734
|
+
|
|
3735
|
+
it('handles navigation when permission check passes but no onNavigate', async () => {
|
|
3736
|
+
const user = userEvent.setup();
|
|
3737
|
+
|
|
3738
|
+
delete (window as any).location;
|
|
3739
|
+
window.location = { href: '' } as any;
|
|
3740
|
+
|
|
3741
|
+
renderWithProviders(
|
|
3742
|
+
<NavigationMenu
|
|
3743
|
+
items={basicNavItems}
|
|
3744
|
+
buttonText="Menu"
|
|
3745
|
+
itemsPreFiltered={true}
|
|
3746
|
+
/>
|
|
3747
|
+
);
|
|
3748
|
+
|
|
3749
|
+
await user.click(screen.getByRole('combobox'));
|
|
3750
|
+
await waitFor(() => {
|
|
3751
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
3752
|
+
}, { interval: 10 });
|
|
3753
|
+
|
|
3754
|
+
const homeItem = screen.getByRole('option', { name: 'Home' });
|
|
3755
|
+
await user.click(homeItem);
|
|
3756
|
+
|
|
3757
|
+
expect(window.location.href).toBe('/');
|
|
3758
|
+
});
|
|
3759
|
+
|
|
3760
|
+
it('handles permission check with non-string permission values', async () => {
|
|
3761
|
+
const user = userEvent.setup();
|
|
3762
|
+
|
|
3763
|
+
const itemsWithInvalidPermissions: NavigationItem[] = [
|
|
3764
|
+
{
|
|
3765
|
+
id: 'test',
|
|
3766
|
+
label: 'Test',
|
|
3767
|
+
href: '/test',
|
|
3768
|
+
permissions: ['valid-permission', 123 as any, null as any, undefined as any]
|
|
3769
|
+
},
|
|
3770
|
+
];
|
|
3771
|
+
|
|
3772
|
+
mockUsePermissions.mockReturnValue({
|
|
3773
|
+
permissions: {
|
|
3774
|
+
'valid-permission': true,
|
|
3775
|
+
'read:page.test': true,
|
|
3776
|
+
} as any,
|
|
3777
|
+
isLoading: false,
|
|
3778
|
+
error: null,
|
|
3779
|
+
hasPermission: vi.fn(() => true),
|
|
3780
|
+
hasAnyPermission: vi.fn((ps: any[]) => {
|
|
3781
|
+
// Filter out non-string permissions
|
|
3782
|
+
const validPerms = ps.filter((p): p is string => typeof p === 'string');
|
|
3783
|
+
return validPerms.some(p => p === 'valid-permission' || p === 'read:page.test');
|
|
3784
|
+
}),
|
|
3785
|
+
hasAllPermissions: vi.fn(() => true),
|
|
3786
|
+
refetch: vi.fn(),
|
|
3787
|
+
});
|
|
3788
|
+
|
|
3789
|
+
renderWithProviders(
|
|
3790
|
+
<NavigationMenu
|
|
3791
|
+
items={itemsWithInvalidPermissions}
|
|
3792
|
+
onNavigate={mockNavigate}
|
|
3793
|
+
buttonText="Menu"
|
|
3794
|
+
/>
|
|
3795
|
+
);
|
|
3796
|
+
|
|
3797
|
+
await user.click(screen.getByRole('combobox'));
|
|
3798
|
+
await waitFor(() => {
|
|
3799
|
+
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
3800
|
+
}, { interval: 10 });
|
|
3801
|
+
});
|
|
3802
|
+
|
|
3803
|
+
it('handles role check with non-string role values', async () => {
|
|
3804
|
+
const user = userEvent.setup();
|
|
3805
|
+
|
|
3806
|
+
const itemsWithInvalidRoles: NavigationItem[] = [
|
|
3807
|
+
{
|
|
3808
|
+
id: 'test',
|
|
3809
|
+
label: 'Test',
|
|
3810
|
+
href: '/test',
|
|
3811
|
+
roles: ['valid-role', 456 as any, null as any, undefined as any]
|
|
3812
|
+
},
|
|
3813
|
+
];
|
|
3814
|
+
|
|
3815
|
+
mockUseRBAC.mockReturnValue({
|
|
3816
|
+
user: { id: 'test-user', email: 'test@example.com' },
|
|
3817
|
+
globalRole: null,
|
|
3818
|
+
organisationRole: 'valid-role' as any,
|
|
3819
|
+
eventAppRole: null,
|
|
3820
|
+
hasPermission: vi.fn(),
|
|
3821
|
+
hasGlobalPermission: vi.fn(),
|
|
3822
|
+
isSuperAdmin: false,
|
|
3823
|
+
isOrgAdmin: false,
|
|
3824
|
+
isEventAdmin: false,
|
|
3825
|
+
canManageOrganisation: false,
|
|
3826
|
+
canManageEvent: false,
|
|
3827
|
+
isLoading: false,
|
|
3828
|
+
error: null,
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
mockUsePermissions.mockReturnValue({
|
|
3832
|
+
permissions: {
|
|
3833
|
+
'read:page.test': true,
|
|
3834
|
+
} as any,
|
|
3835
|
+
isLoading: false,
|
|
3836
|
+
error: null,
|
|
3837
|
+
hasPermission: vi.fn(() => true),
|
|
3838
|
+
hasAnyPermission: vi.fn(() => true),
|
|
3839
|
+
hasAllPermissions: vi.fn(() => true),
|
|
3840
|
+
refetch: vi.fn(),
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
renderWithProviders(
|
|
3844
|
+
<NavigationMenu
|
|
3845
|
+
items={itemsWithInvalidRoles}
|
|
3846
|
+
onNavigate={mockNavigate}
|
|
3847
|
+
buttonText="Menu"
|
|
3848
|
+
/>
|
|
3849
|
+
);
|
|
3850
|
+
|
|
3851
|
+
await user.click(screen.getByRole('combobox'));
|
|
3852
|
+
await waitFor(() => {
|
|
3853
|
+
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
3854
|
+
}, { interval: 10 });
|
|
3855
|
+
});
|
|
3856
|
+
});
|
|
1537
3857
|
});
|