@jmruthers/pace-core 0.6.2 → 0.6.3
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 +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-THFPBKTP.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-IAGWF3ZG.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/{chunk-SFZUDBL5.js → chunk-2T2IG7T7.js} +70 -56
- package/dist/chunk-2T2IG7T7.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-6Z7LTB3D.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-6Z7LTB3D.js.map} +1 -1
- package/dist/{chunk-6J4GEEJR.js → chunk-CNCQDFLN.js} +53 -27
- package/dist/chunk-CNCQDFLN.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-DWUBLJJM.js} +361 -187
- package/dist/chunk-DWUBLJJM.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-HFZBI76P.js} +4 -4
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-3XC4CPTD.js → chunk-PQBSKX33.js} +244 -5727
- package/dist/chunk-PQBSKX33.js.map +1 -0
- package/dist/chunk-QRPVRXYT.js +226 -0
- package/dist/chunk-QRPVRXYT.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-RWEBCB47.js} +129 -387
- package/dist/chunk-RWEBCB47.js.map +1 -0
- package/dist/{chunk-XWQCNGTQ.js → chunk-YDQHOZNA.js} +173 -79
- package/dist/chunk-YDQHOZNA.js.map +1 -0
- package/dist/{chunk-NECFR5MM.js → chunk-ZNIWI3UC.js} +562 -644
- package/dist/chunk-ZNIWI3UC.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-3JNZKUTX.js +9 -0
- package/dist/contextValidator-3JNZKUTX.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +3 -3
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +559 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +135 -33
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +186 -54
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-THFPBKTP.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-IAGWF3ZG.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-HFZBI76P.js.map} +0 -0
- /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
|
@@ -1,784 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file OrganisationSelector Component Tests
|
|
3
|
-
* @package @jmruthers/pace-core
|
|
4
|
-
* @module Components/OrganisationSelector
|
|
5
|
-
* @since 0.4.0
|
|
6
|
-
*
|
|
7
|
-
* Comprehensive test suite for the OrganisationSelector component covering all functionality,
|
|
8
|
-
* RBAC behavior, error handling, and accessibility features.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { screen, waitFor } from '@testing-library/react';
|
|
13
|
-
import userEvent from '@testing-library/user-event';
|
|
14
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
-
import { OrganisationSelector, OrganisationSelectorProps } from './OrganisationSelector';
|
|
16
|
-
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
17
|
-
import type { Organisation } from '../../types/organisation';
|
|
18
|
-
import type { SelectProps, SelectContentProps, SelectItemProps, SelectTriggerProps, SelectValueProps } from '../Select';
|
|
19
|
-
import type { ButtonProps } from '../Button/Button';
|
|
20
|
-
import type { LoadingSpinnerProps } from '../LoadingSpinner/LoadingSpinner';
|
|
21
|
-
|
|
22
|
-
// Mock the useOrganisationService hook (used internally by useOrganisations)
|
|
23
|
-
const mockUseOrganisationService = vi.fn();
|
|
24
|
-
vi.mock('../../hooks/services/useOrganisationService', () => ({
|
|
25
|
-
useOrganisationService: () => mockUseOrganisationService(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
// Mock the useOrganisations hook
|
|
29
|
-
const mockUseOrganisations = vi.fn();
|
|
30
|
-
vi.mock('../../hooks/useOrganisations', () => ({
|
|
31
|
-
useOrganisations: () => mockUseOrganisations(),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// Mock child components
|
|
35
|
-
let mockOnValueChange: ((value: string) => void) | null = null;
|
|
36
|
-
|
|
37
|
-
vi.mock('../Select', () => ({
|
|
38
|
-
Select: ({ children, value, onValueChange, disabled }: SelectProps & { value?: string; onValueChange?: (value: string) => void; disabled?: boolean }) => {
|
|
39
|
-
// Store the onValueChange callback for SelectItem to use
|
|
40
|
-
mockOnValueChange = onValueChange || null;
|
|
41
|
-
return (
|
|
42
|
-
<div data-testid="select" data-value={value} data-disabled={disabled}>
|
|
43
|
-
{children}
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
},
|
|
47
|
-
SelectContent: ({ children }: SelectContentProps) => (
|
|
48
|
-
<div data-testid="select-content">{children}</div>
|
|
49
|
-
),
|
|
50
|
-
SelectItem: ({ children, value, disabled, className }: SelectItemProps) => (
|
|
51
|
-
<div
|
|
52
|
-
data-testid={`select-item-${value}`}
|
|
53
|
-
data-disabled={disabled}
|
|
54
|
-
className={className}
|
|
55
|
-
onClick={() => !disabled && mockOnValueChange?.(value)}
|
|
56
|
-
>
|
|
57
|
-
{children}
|
|
58
|
-
</div>
|
|
59
|
-
),
|
|
60
|
-
SelectTrigger: ({ children, className }: SelectTriggerProps) => (
|
|
61
|
-
<button data-testid="select-trigger" className={className}>
|
|
62
|
-
{children}
|
|
63
|
-
</button>
|
|
64
|
-
),
|
|
65
|
-
SelectValue: ({ placeholder }: SelectValueProps) => (
|
|
66
|
-
<span data-testid="select-value">{placeholder}</span>
|
|
67
|
-
),
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
vi.mock('../Alert/Alert', () => ({
|
|
71
|
-
Alert: ({ children, variant }: React.HTMLAttributes<HTMLDivElement> & { variant?: "default" | "destructive" | "inline" }) => (
|
|
72
|
-
<div data-testid="alert" data-variant={variant}>
|
|
73
|
-
{children}
|
|
74
|
-
</div>
|
|
75
|
-
),
|
|
76
|
-
AlertDescription: ({ children }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
77
|
-
<div data-testid="alert-description">{children}</div>
|
|
78
|
-
),
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
vi.mock('../Button/Button', () => ({
|
|
82
|
-
Button: ({ children, onClick, disabled, variant, size, className }: ButtonProps) => (
|
|
83
|
-
<button
|
|
84
|
-
data-testid="button"
|
|
85
|
-
onClick={onClick}
|
|
86
|
-
disabled={disabled}
|
|
87
|
-
data-variant={variant}
|
|
88
|
-
data-size={size}
|
|
89
|
-
className={className}
|
|
90
|
-
>
|
|
91
|
-
{children}
|
|
92
|
-
</button>
|
|
93
|
-
),
|
|
94
|
-
}));
|
|
95
|
-
|
|
96
|
-
vi.mock('../LoadingSpinner/LoadingSpinner', () => ({
|
|
97
|
-
LoadingSpinner: ({ size }: LoadingSpinnerProps) => (
|
|
98
|
-
<div data-testid="loading-spinner" data-size={size}>Loading...</div>
|
|
99
|
-
),
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
// Test data
|
|
103
|
-
const mockOrganisations: Organisation[] = [
|
|
104
|
-
{
|
|
105
|
-
id: 'org-1',
|
|
106
|
-
name: 'acme-corp',
|
|
107
|
-
display_name: 'Acme Corporation',
|
|
108
|
-
description: 'Leading technology company',
|
|
109
|
-
subscription_tier: 'premium',
|
|
110
|
-
settings: {},
|
|
111
|
-
is_active: true,
|
|
112
|
-
created_at: '2023-01-01T00:00:00Z',
|
|
113
|
-
updated_at: '2023-01-01T00:00:00Z',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: 'org-2',
|
|
117
|
-
name: 'beta-inc',
|
|
118
|
-
display_name: 'Beta Inc',
|
|
119
|
-
description: 'Innovation focused startup',
|
|
120
|
-
subscription_tier: 'standard',
|
|
121
|
-
settings: {},
|
|
122
|
-
is_active: true,
|
|
123
|
-
created_at: '2023-01-02T00:00:00Z',
|
|
124
|
-
updated_at: '2023-01-02T00:00:00Z',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'org-3',
|
|
128
|
-
name: 'gamma-ltd',
|
|
129
|
-
display_name: 'Gamma Ltd',
|
|
130
|
-
description: 'Global enterprise solutions',
|
|
131
|
-
subscription_tier: 'enterprise',
|
|
132
|
-
settings: {},
|
|
133
|
-
is_active: true,
|
|
134
|
-
created_at: '2023-01-03T00:00:00Z',
|
|
135
|
-
updated_at: '2023-01-03T00:00:00Z',
|
|
136
|
-
},
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
const mockSelectedOrganisation = mockOrganisations[0];
|
|
140
|
-
|
|
141
|
-
const defaultMockContext = {
|
|
142
|
-
organisations: mockOrganisations,
|
|
143
|
-
selectedOrganisation: mockSelectedOrganisation,
|
|
144
|
-
isLoading: false,
|
|
145
|
-
error: null,
|
|
146
|
-
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
147
|
-
getUserRole: vi.fn().mockReturnValue('admin'),
|
|
148
|
-
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
149
|
-
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
describe('OrganisationSelector Component', () => {
|
|
153
|
-
beforeEach(() => {
|
|
154
|
-
vi.clearAllMocks();
|
|
155
|
-
|
|
156
|
-
// Re-setup the default mock context after clearing mocks
|
|
157
|
-
const mockContext = {
|
|
158
|
-
organisations: mockOrganisations,
|
|
159
|
-
selectedOrganisation: mockSelectedOrganisation,
|
|
160
|
-
isLoading: false,
|
|
161
|
-
error: null,
|
|
162
|
-
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
163
|
-
getUserRole: vi.fn().mockReturnValue('admin'),
|
|
164
|
-
validateOrganisationAccess: vi.fn().mockReturnValue(true), // Ensure all orgs are accessible by default
|
|
165
|
-
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
mockUseOrganisations.mockReturnValue(mockContext);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
vi.clearAllMocks();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Basic rendering tests
|
|
176
|
-
describe('Rendering', () => {
|
|
177
|
-
it('renders with default props', () => {
|
|
178
|
-
renderWithProviders(<OrganisationSelector />);
|
|
179
|
-
|
|
180
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
181
|
-
expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
|
|
182
|
-
expect(screen.getByTestId('select-value')).toHaveTextContent('Select organisation');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('renders with custom placeholder', () => {
|
|
186
|
-
renderWithProviders(<OrganisationSelector placeholder="Choose organisation..." />);
|
|
187
|
-
|
|
188
|
-
expect(screen.getByTestId('select-value')).toHaveTextContent('Choose organisation...');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('renders with custom className', () => {
|
|
192
|
-
renderWithProviders(<OrganisationSelector className="custom-selector" />);
|
|
193
|
-
|
|
194
|
-
const container = screen.getByTestId('select').parentElement;
|
|
195
|
-
expect(container).toHaveClass('custom-selector');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('renders with proper DOM structure', () => {
|
|
199
|
-
renderWithProviders(<OrganisationSelector />);
|
|
200
|
-
|
|
201
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
202
|
-
expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
|
|
203
|
-
expect(screen.getByTestId('select-content')).toBeInTheDocument();
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Loading state tests
|
|
208
|
-
describe('Loading State', () => {
|
|
209
|
-
it('renders loading state when organisations are loading', () => {
|
|
210
|
-
mockUseOrganisations.mockReturnValue({
|
|
211
|
-
...defaultMockContext,
|
|
212
|
-
isLoading: true,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
renderWithProviders(<OrganisationSelector />);
|
|
216
|
-
|
|
217
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
218
|
-
expect(screen.getByText('Loading organisations...')).toBeInTheDocument();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('renders compact loading state', () => {
|
|
222
|
-
mockUseOrganisations.mockReturnValue({
|
|
223
|
-
...defaultMockContext,
|
|
224
|
-
isLoading: true,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
renderWithProviders(<OrganisationSelector compact={true} />);
|
|
228
|
-
|
|
229
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
230
|
-
expect(screen.getAllByText('Loading...')).toHaveLength(2); // Spinner + text
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('shows loading spinner in trigger when switching organisations', () => {
|
|
234
|
-
// Test the loading state directly by mocking the loading state
|
|
235
|
-
mockUseOrganisations.mockReturnValue({
|
|
236
|
-
...defaultMockContext,
|
|
237
|
-
isLoading: true,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
renderWithProviders(<OrganisationSelector />);
|
|
241
|
-
|
|
242
|
-
// Should show loading state
|
|
243
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Error state tests
|
|
248
|
-
describe('Error State', () => {
|
|
249
|
-
it('renders error state when organisations fail to load', () => {
|
|
250
|
-
const error = new Error('Failed to load organisations');
|
|
251
|
-
mockUseOrganisations.mockReturnValue({
|
|
252
|
-
...defaultMockContext,
|
|
253
|
-
error,
|
|
254
|
-
organisations: [],
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
renderWithProviders(<OrganisationSelector />);
|
|
258
|
-
|
|
259
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
260
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('Failed to load organisations: Failed to load organisations');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('shows retry button when showRetryButton is true', () => {
|
|
264
|
-
const error = new Error('Failed to load organisations');
|
|
265
|
-
mockUseOrganisations.mockReturnValue({
|
|
266
|
-
...defaultMockContext,
|
|
267
|
-
error,
|
|
268
|
-
organisations: [],
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
renderWithProviders(<OrganisationSelector showRetryButton={true} />);
|
|
272
|
-
|
|
273
|
-
expect(screen.getByTestId('button')).toBeInTheDocument();
|
|
274
|
-
expect(screen.getByTestId('button')).toHaveTextContent('Retry');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('does not show retry button when showRetryButton is false', () => {
|
|
278
|
-
const error = new Error('Failed to load organisations');
|
|
279
|
-
mockUseOrganisations.mockReturnValue({
|
|
280
|
-
...defaultMockContext,
|
|
281
|
-
error,
|
|
282
|
-
organisations: [],
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
renderWithProviders(<OrganisationSelector showRetryButton={false} />);
|
|
286
|
-
|
|
287
|
-
expect(screen.queryByTestId('button')).not.toBeInTheDocument();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('handles retry button click', async () => {
|
|
291
|
-
const user = userEvent.setup();
|
|
292
|
-
const refreshOrganisations = vi.fn().mockResolvedValue(undefined);
|
|
293
|
-
|
|
294
|
-
const error = new Error('Failed to load organisations');
|
|
295
|
-
mockUseOrganisations.mockReturnValue({
|
|
296
|
-
...defaultMockContext,
|
|
297
|
-
error,
|
|
298
|
-
organisations: [],
|
|
299
|
-
refreshOrganisations,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
renderWithProviders(<OrganisationSelector showRetryButton={true} />);
|
|
303
|
-
|
|
304
|
-
const retryButton = screen.getByTestId('button');
|
|
305
|
-
await user.click(retryButton);
|
|
306
|
-
|
|
307
|
-
expect(refreshOrganisations).toHaveBeenCalledTimes(1);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Empty state tests
|
|
312
|
-
describe('Empty State', () => {
|
|
313
|
-
it('renders no organisations message when showNoOrganisationsMessage is true', () => {
|
|
314
|
-
mockUseOrganisations.mockReturnValue({
|
|
315
|
-
...defaultMockContext,
|
|
316
|
-
organisations: [],
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
renderWithProviders(<OrganisationSelector showNoOrganisationsMessage={true} />);
|
|
320
|
-
|
|
321
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
322
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('No organisations available. Please contact your administrator to be added to an organisation.');
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('shows check again button when no organisations and showRetryButton is true', () => {
|
|
326
|
-
mockUseOrganisations.mockReturnValue({
|
|
327
|
-
...defaultMockContext,
|
|
328
|
-
organisations: [],
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
renderWithProviders(<OrganisationSelector showRetryButton={true} />);
|
|
332
|
-
|
|
333
|
-
expect(screen.getByTestId('button')).toBeInTheDocument();
|
|
334
|
-
expect(screen.getByTestId('button')).toHaveTextContent('Check Again');
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('returns null when no organisations and showNoOrganisationsMessage is false', () => {
|
|
338
|
-
mockUseOrganisations.mockReturnValue({
|
|
339
|
-
...defaultMockContext,
|
|
340
|
-
organisations: [],
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
const { container } = renderWithProviders(<OrganisationSelector showNoOrganisationsMessage={false} />);
|
|
344
|
-
|
|
345
|
-
expect(container.firstChild).toBeNull();
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// Organisation switching tests
|
|
350
|
-
describe('Organisation Switching', () => {
|
|
351
|
-
it('renders all available organisations', () => {
|
|
352
|
-
renderWithProviders(<OrganisationSelector />);
|
|
353
|
-
|
|
354
|
-
mockOrganisations.forEach(org => {
|
|
355
|
-
expect(screen.getByTestId(`select-item-${org.id}`)).toBeInTheDocument();
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('displays organisation names and descriptions', () => {
|
|
360
|
-
renderWithProviders(<OrganisationSelector />);
|
|
361
|
-
|
|
362
|
-
expect(screen.getByText('Acme Corporation')).toBeInTheDocument();
|
|
363
|
-
expect(screen.getByText('Leading technology company')).toBeInTheDocument();
|
|
364
|
-
expect(screen.getByText('Beta Inc')).toBeInTheDocument();
|
|
365
|
-
expect(screen.getByText('Innovation focused startup')).toBeInTheDocument();
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('handles organisation selection', async () => {
|
|
369
|
-
const user = userEvent.setup();
|
|
370
|
-
const onOrganisationChange = vi.fn();
|
|
371
|
-
const switchOrganisation = vi.fn().mockResolvedValue(undefined);
|
|
372
|
-
const validateOrganisationAccess = vi.fn().mockReturnValue(true);
|
|
373
|
-
|
|
374
|
-
mockUseOrganisations.mockReturnValue({
|
|
375
|
-
organisations: mockOrganisations,
|
|
376
|
-
selectedOrganisation: mockSelectedOrganisation,
|
|
377
|
-
isLoading: false,
|
|
378
|
-
error: null,
|
|
379
|
-
switchOrganisation,
|
|
380
|
-
getUserRole: vi.fn().mockReturnValue('admin'),
|
|
381
|
-
validateOrganisationAccess,
|
|
382
|
-
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
renderWithProviders(
|
|
386
|
-
<OrganisationSelector onOrganisationChange={onOrganisationChange} />
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
const selectItem = screen.getByTestId('select-item-org-2');
|
|
390
|
-
await user.click(selectItem);
|
|
391
|
-
|
|
392
|
-
expect(switchOrganisation).toHaveBeenCalledWith('org-2');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('calls onOrganisationChange callback after successful switch', async () => {
|
|
396
|
-
const user = userEvent.setup();
|
|
397
|
-
const onOrganisationChange = vi.fn();
|
|
398
|
-
const switchOrganisation = vi.fn().mockResolvedValue(undefined);
|
|
399
|
-
const validateOrganisationAccess = vi.fn().mockReturnValue(true);
|
|
400
|
-
|
|
401
|
-
mockUseOrganisations.mockReturnValue({
|
|
402
|
-
organisations: mockOrganisations,
|
|
403
|
-
selectedOrganisation: mockSelectedOrganisation,
|
|
404
|
-
isLoading: false,
|
|
405
|
-
error: null,
|
|
406
|
-
switchOrganisation,
|
|
407
|
-
getUserRole: vi.fn().mockReturnValue('admin'),
|
|
408
|
-
validateOrganisationAccess,
|
|
409
|
-
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
renderWithProviders(
|
|
413
|
-
<OrganisationSelector onOrganisationChange={onOrganisationChange} />
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
const selectItem = screen.getByTestId('select-item-org-2');
|
|
417
|
-
await user.click(selectItem);
|
|
418
|
-
|
|
419
|
-
await waitFor(() => {
|
|
420
|
-
expect(onOrganisationChange).toHaveBeenCalledWith(mockOrganisations[1]);
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('handles organisation switch errors', () => {
|
|
425
|
-
// Test error state directly by mocking an error
|
|
426
|
-
const error = new Error('Access denied');
|
|
427
|
-
mockUseOrganisations.mockReturnValue({
|
|
428
|
-
...defaultMockContext,
|
|
429
|
-
error,
|
|
430
|
-
organisations: [],
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
renderWithProviders(<OrganisationSelector />);
|
|
434
|
-
|
|
435
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
436
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('Failed to load organisations: Access denied');
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('validates organisation access before switching', async () => {
|
|
440
|
-
const user = userEvent.setup();
|
|
441
|
-
const validateOrganisationAccess = vi.fn().mockReturnValue(false);
|
|
442
|
-
const switchOrganisation = vi.fn();
|
|
443
|
-
|
|
444
|
-
mockUseOrganisations.mockReturnValue({
|
|
445
|
-
...defaultMockContext,
|
|
446
|
-
validateOrganisationAccess,
|
|
447
|
-
switchOrganisation,
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
renderWithProviders(<OrganisationSelector />);
|
|
451
|
-
|
|
452
|
-
const selectItem = screen.getByTestId('select-item-org-2');
|
|
453
|
-
await user.click(selectItem);
|
|
454
|
-
|
|
455
|
-
expect(validateOrganisationAccess).toHaveBeenCalledWith('org-2');
|
|
456
|
-
expect(switchOrganisation).not.toHaveBeenCalled();
|
|
457
|
-
});
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// RBAC and permission tests
|
|
461
|
-
describe('RBAC and Permissions', () => {
|
|
462
|
-
it('disables organisations without access', () => {
|
|
463
|
-
const validateOrganisationAccess = vi.fn((orgId: string) => orgId !== 'org-2');
|
|
464
|
-
|
|
465
|
-
mockUseOrganisations.mockReturnValue({
|
|
466
|
-
...defaultMockContext,
|
|
467
|
-
validateOrganisationAccess,
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
renderWithProviders(<OrganisationSelector />);
|
|
471
|
-
|
|
472
|
-
expect(screen.getByTestId('select-item-org-1')).not.toHaveAttribute('data-disabled', 'true');
|
|
473
|
-
expect(screen.getByTestId('select-item-org-2')).toHaveAttribute('data-disabled', 'true');
|
|
474
|
-
expect(screen.getByTestId('select-item-org-3')).not.toHaveAttribute('data-disabled', 'true');
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('shows user role when showRole is true', () => {
|
|
478
|
-
const getUserRole = vi.fn().mockReturnValue('admin');
|
|
479
|
-
|
|
480
|
-
mockUseOrganisations.mockReturnValue({
|
|
481
|
-
...defaultMockContext,
|
|
482
|
-
getUserRole,
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
renderWithProviders(<OrganisationSelector showRole={true} />);
|
|
486
|
-
|
|
487
|
-
// The role should be displayed in the select items
|
|
488
|
-
expect(screen.getAllByText('admin')).toHaveLength(3);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('does not show role when showRole is false', () => {
|
|
492
|
-
const getUserRole = vi.fn().mockReturnValue('admin');
|
|
493
|
-
|
|
494
|
-
mockUseOrganisations.mockReturnValue({
|
|
495
|
-
...defaultMockContext,
|
|
496
|
-
getUserRole,
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
renderWithProviders(<OrganisationSelector showRole={false} />);
|
|
500
|
-
|
|
501
|
-
expect(screen.queryByText('admin')).not.toBeInTheDocument();
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it('formats role names correctly', () => {
|
|
505
|
-
const getUserRole = vi.fn().mockReturnValue('super_admin');
|
|
506
|
-
|
|
507
|
-
mockUseOrganisations.mockReturnValue({
|
|
508
|
-
...defaultMockContext,
|
|
509
|
-
getUserRole,
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
renderWithProviders(<OrganisationSelector showRole={true} />);
|
|
513
|
-
|
|
514
|
-
expect(screen.getAllByText('super admin')).toHaveLength(3);
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// Compact mode tests
|
|
519
|
-
describe('Compact Mode', () => {
|
|
520
|
-
it('hides descriptions in compact mode', () => {
|
|
521
|
-
renderWithProviders(<OrganisationSelector compact={true} />);
|
|
522
|
-
|
|
523
|
-
expect(screen.queryByText('Leading technology company')).not.toBeInTheDocument();
|
|
524
|
-
expect(screen.queryByText('Innovation focused startup')).not.toBeInTheDocument();
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it('shows descriptions when not in compact mode', () => {
|
|
528
|
-
renderWithProviders(<OrganisationSelector compact={false} />);
|
|
529
|
-
|
|
530
|
-
expect(screen.getByText('Leading technology company')).toBeInTheDocument();
|
|
531
|
-
expect(screen.getByText('Innovation focused startup')).toBeInTheDocument();
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// Disabled state tests
|
|
536
|
-
describe('Disabled State', () => {
|
|
537
|
-
it('disables the selector when disabled prop is true', () => {
|
|
538
|
-
renderWithProviders(<OrganisationSelector disabled={true} />);
|
|
539
|
-
|
|
540
|
-
expect(screen.getByTestId('select')).toHaveAttribute('data-disabled', 'true');
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it('enables the selector when disabled prop is false', () => {
|
|
544
|
-
renderWithProviders(<OrganisationSelector disabled={false} />);
|
|
545
|
-
|
|
546
|
-
expect(screen.getByTestId('select')).toHaveAttribute('data-disabled', 'false');
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('disables selector during loading', () => {
|
|
550
|
-
// Test the disabled state directly by mocking the loading state
|
|
551
|
-
mockUseOrganisations.mockReturnValue({
|
|
552
|
-
...defaultMockContext,
|
|
553
|
-
isLoading: true,
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
renderWithProviders(<OrganisationSelector />);
|
|
557
|
-
|
|
558
|
-
// Should show loading state instead of selector
|
|
559
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Accessibility tests
|
|
564
|
-
describe('Accessibility', () => {
|
|
565
|
-
it('has proper ARIA attributes', () => {
|
|
566
|
-
renderWithProviders(<OrganisationSelector />);
|
|
567
|
-
|
|
568
|
-
const select = screen.getByTestId('select');
|
|
569
|
-
expect(select).toBeInTheDocument();
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('provides screen reader accessible content', () => {
|
|
573
|
-
renderWithProviders(<OrganisationSelector />);
|
|
574
|
-
|
|
575
|
-
expect(screen.getByText('Acme Corporation')).toBeInTheDocument();
|
|
576
|
-
expect(screen.getByText('Beta Inc')).toBeInTheDocument();
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it('shows loading state to screen readers', () => {
|
|
580
|
-
mockUseOrganisations.mockReturnValue({
|
|
581
|
-
...defaultMockContext,
|
|
582
|
-
isLoading: true,
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
renderWithProviders(<OrganisationSelector />);
|
|
586
|
-
|
|
587
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
588
|
-
expect(screen.getByText('Loading organisations...')).toBeInTheDocument();
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('announces errors to screen readers', () => {
|
|
592
|
-
const error = new Error('Failed to load organisations');
|
|
593
|
-
mockUseOrganisations.mockReturnValue({
|
|
594
|
-
...defaultMockContext,
|
|
595
|
-
error,
|
|
596
|
-
organisations: [],
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
renderWithProviders(<OrganisationSelector />);
|
|
600
|
-
|
|
601
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
602
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('Failed to load organisations: Failed to load organisations');
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
// Error handling tests
|
|
607
|
-
describe('Error Handling', () => {
|
|
608
|
-
it('handles switch errors gracefully', () => {
|
|
609
|
-
// Test error state directly by mocking an error
|
|
610
|
-
const error = new Error('Network error');
|
|
611
|
-
mockUseOrganisations.mockReturnValue({
|
|
612
|
-
...defaultMockContext,
|
|
613
|
-
error,
|
|
614
|
-
organisations: [],
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
renderWithProviders(<OrganisationSelector />);
|
|
618
|
-
|
|
619
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
620
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('Failed to load organisations: Network error');
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it('handles retry errors gracefully', () => {
|
|
624
|
-
// Test error state directly by mocking an error
|
|
625
|
-
const error = new Error('Retry failed');
|
|
626
|
-
mockUseOrganisations.mockReturnValue({
|
|
627
|
-
...defaultMockContext,
|
|
628
|
-
error,
|
|
629
|
-
organisations: [],
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
renderWithProviders(<OrganisationSelector showRetryButton={true} />);
|
|
633
|
-
|
|
634
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
635
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('Failed to load organisations: Retry failed');
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it('clears errors on successful retry', async () => {
|
|
639
|
-
const user = userEvent.setup();
|
|
640
|
-
const refreshOrganisations = vi.fn().mockResolvedValue(undefined);
|
|
641
|
-
|
|
642
|
-
const error = new Error('Failed to load organisations');
|
|
643
|
-
mockUseOrganisations.mockReturnValue({
|
|
644
|
-
...defaultMockContext,
|
|
645
|
-
error,
|
|
646
|
-
organisations: [],
|
|
647
|
-
refreshOrganisations,
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
renderWithProviders(<OrganisationSelector showRetryButton={true} />);
|
|
651
|
-
|
|
652
|
-
const retryButton = screen.getByTestId('button');
|
|
653
|
-
await user.click(retryButton);
|
|
654
|
-
|
|
655
|
-
await waitFor(() => {
|
|
656
|
-
expect(refreshOrganisations).toHaveBeenCalledTimes(1);
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// Edge cases and prop validation tests
|
|
662
|
-
describe('Edge Cases and Prop Validation', () => {
|
|
663
|
-
it('handles empty organisations array', () => {
|
|
664
|
-
mockUseOrganisations.mockReturnValue({
|
|
665
|
-
...defaultMockContext,
|
|
666
|
-
organisations: [],
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
renderWithProviders(<OrganisationSelector showNoOrganisationsMessage={true} />);
|
|
670
|
-
|
|
671
|
-
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
672
|
-
expect(screen.getByTestId('alert-description')).toHaveTextContent('No organisations available');
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
it('handles undefined onOrganisationChange callback', async () => {
|
|
676
|
-
const user = userEvent.setup();
|
|
677
|
-
|
|
678
|
-
renderWithProviders(<OrganisationSelector />);
|
|
679
|
-
|
|
680
|
-
const selectItem = screen.getByTestId('select-item-org-2');
|
|
681
|
-
await user.click(selectItem);
|
|
682
|
-
|
|
683
|
-
// Should not throw error
|
|
684
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it('handles null selectedOrganisation gracefully', () => {
|
|
688
|
-
mockUseOrganisations.mockReturnValue({
|
|
689
|
-
...defaultMockContext,
|
|
690
|
-
selectedOrganisation: null,
|
|
691
|
-
isLoading: true, // This should trigger loading state
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// This should render the loading state instead of crashing
|
|
695
|
-
renderWithProviders(<OrganisationSelector />);
|
|
696
|
-
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
it('handles very long organisation names', () => {
|
|
700
|
-
const longNameOrg = {
|
|
701
|
-
...mockOrganisations[0],
|
|
702
|
-
display_name: 'A'.repeat(1000),
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
mockUseOrganisations.mockReturnValue({
|
|
706
|
-
...defaultMockContext,
|
|
707
|
-
organisations: [longNameOrg],
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
renderWithProviders(<OrganisationSelector />);
|
|
711
|
-
|
|
712
|
-
expect(screen.getByText(longNameOrg.display_name)).toBeInTheDocument();
|
|
713
|
-
});
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
// Performance tests
|
|
717
|
-
describe('Performance', () => {
|
|
718
|
-
it('renders quickly with many organisations', () => {
|
|
719
|
-
const manyOrgs = Array.from({ length: 100 }, (_, i) => ({
|
|
720
|
-
...mockOrganisations[0],
|
|
721
|
-
id: `org-${i}`,
|
|
722
|
-
display_name: `Organisation ${i}`,
|
|
723
|
-
}));
|
|
724
|
-
|
|
725
|
-
mockUseOrganisations.mockReturnValue({
|
|
726
|
-
...defaultMockContext,
|
|
727
|
-
organisations: manyOrgs,
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
const startTime = performance.now();
|
|
731
|
-
renderWithProviders(<OrganisationSelector />);
|
|
732
|
-
const endTime = performance.now();
|
|
733
|
-
|
|
734
|
-
expect(endTime - startTime).toBeLessThan(500); // Should render in under 500ms
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it('handles rapid organisation switches', async () => {
|
|
738
|
-
const user = userEvent.setup();
|
|
739
|
-
|
|
740
|
-
renderWithProviders(<OrganisationSelector />);
|
|
741
|
-
|
|
742
|
-
// Rapid switches
|
|
743
|
-
for (let i = 0; i < 5; i++) {
|
|
744
|
-
const selectItem = screen.getByTestId(`select-item-org-${(i % 3) + 1}`);
|
|
745
|
-
await user.click(selectItem);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Integration tests
|
|
753
|
-
describe('Integration Scenarios', () => {
|
|
754
|
-
it('works with all props enabled', () => {
|
|
755
|
-
renderWithProviders(
|
|
756
|
-
<OrganisationSelector
|
|
757
|
-
placeholder="Choose organisation..."
|
|
758
|
-
className="custom-class"
|
|
759
|
-
showRole={true}
|
|
760
|
-
compact={false}
|
|
761
|
-
showRetryButton={true}
|
|
762
|
-
showNoOrganisationsMessage={true}
|
|
763
|
-
disabled={false}
|
|
764
|
-
/>
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
768
|
-
expect(screen.getByText('Choose organisation...')).toBeInTheDocument();
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
it('works in minimal configuration', () => {
|
|
772
|
-
renderWithProviders(
|
|
773
|
-
<OrganisationSelector
|
|
774
|
-
showRole={false}
|
|
775
|
-
compact={true}
|
|
776
|
-
showRetryButton={false}
|
|
777
|
-
showNoOrganisationsMessage={false}
|
|
778
|
-
/>
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
expect(screen.getByTestId('select')).toBeInTheDocument();
|
|
782
|
-
});
|
|
783
|
-
});
|
|
784
|
-
});
|