@jmruthers/pace-core 0.5.114 → 0.5.116
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/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
- package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +43 -16
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +32 -17
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ImportModal.tsx +25 -2
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.test.tsx +8 -7
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +11 -12
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-4OX5PXHX.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-5YIZFEUQ.js.map +0 -1
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ProtectedRoute Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/ProtectedRoute
|
|
5
|
+
* @since 0.6.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for the ProtectedRoute component following TEST_STANDARD.md.
|
|
8
|
+
* Tests focus on behavior: authentication, session restoration, event management, and navigation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import { ProtectedRoute, ProtectedRouteProps } from './ProtectedRoute';
|
|
15
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
16
|
+
|
|
17
|
+
// Mock React Router components
|
|
18
|
+
vi.mock('react-router-dom', async () => {
|
|
19
|
+
const actual = await vi.importActual('react-router-dom');
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
Navigate: ({ to, replace }: { to: string; replace?: boolean }) => (
|
|
23
|
+
<div data-testid="navigate" data-to={to} data-replace={replace?.toString()}>
|
|
24
|
+
Navigate to {to}
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
Outlet: () => <div data-testid="outlet">Protected Content</div>,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Mock pace-core hooks and components
|
|
32
|
+
vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
|
|
33
|
+
useUnifiedAuth: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../../hooks/useSessionRestoration', () => ({
|
|
37
|
+
useSessionRestoration: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock('../../hooks/useEvents', () => ({
|
|
41
|
+
useEvents: vi.fn(),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('../LoadingSpinner/LoadingSpinner', () => ({
|
|
45
|
+
LoadingSpinner: () => <div data-testid="loading-spinner" role="status">Loading...</div>,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('../SessionRestorationLoader', () => ({
|
|
49
|
+
SessionRestorationLoader: () => (
|
|
50
|
+
<div data-testid="session-restoration-loader">
|
|
51
|
+
<div role="status">Restoring session...</div>
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock('../Alert/Alert', () => ({
|
|
57
|
+
Alert: ({ children, variant, className }: any) => (
|
|
58
|
+
<div data-testid="alert" data-variant={variant} className={className}>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
AlertTitle: ({ children }: any) => <div data-testid="alert-title">{children}</div>,
|
|
63
|
+
AlertDescription: ({ children }: any) => (
|
|
64
|
+
<div data-testid="alert-description">{children}</div>
|
|
65
|
+
),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
69
|
+
import { useSessionRestoration } from '../../hooks/useSessionRestoration';
|
|
70
|
+
import { useEvents } from '../../hooks/useEvents';
|
|
71
|
+
|
|
72
|
+
// Type-safe mock implementations
|
|
73
|
+
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
74
|
+
const mockUseSessionRestoration = vi.mocked(useSessionRestoration);
|
|
75
|
+
const mockUseEvents = vi.mocked(useEvents);
|
|
76
|
+
|
|
77
|
+
// Suppress console methods in tests
|
|
78
|
+
const originalConsoleDebug = console.debug;
|
|
79
|
+
const originalConsoleWarn = console.warn;
|
|
80
|
+
|
|
81
|
+
describe('ProtectedRoute Component', () => {
|
|
82
|
+
// Default mock implementations
|
|
83
|
+
const defaultAuthState = {
|
|
84
|
+
isAuthenticated: true,
|
|
85
|
+
authLoading: false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const defaultSessionState = {
|
|
89
|
+
isRestoring: false,
|
|
90
|
+
restorationComplete: true,
|
|
91
|
+
restorationError: null,
|
|
92
|
+
hasTimedOut: false,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const defaultEventsState = {
|
|
96
|
+
selectedEvent: { id: 'event-1', name: 'Test Event' },
|
|
97
|
+
events: [{ id: 'event-1', name: 'Test Event' }],
|
|
98
|
+
isLoading: false,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
console.debug = vi.fn();
|
|
104
|
+
console.warn = vi.fn();
|
|
105
|
+
|
|
106
|
+
// Set default mocks
|
|
107
|
+
mockUseUnifiedAuth.mockReturnValue(defaultAuthState);
|
|
108
|
+
mockUseSessionRestoration.mockReturnValue(defaultSessionState);
|
|
109
|
+
mockUseEvents.mockReturnValue(defaultEventsState);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
console.debug = originalConsoleDebug;
|
|
114
|
+
console.warn = originalConsoleWarn;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Rendering', () => {
|
|
118
|
+
it('renders outlet when user is authenticated and event is selected', () => {
|
|
119
|
+
renderWithProviders(<ProtectedRoute />);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
122
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
|
123
|
+
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders outlet when events exist but none selected (allows event selector visibility)', () => {
|
|
127
|
+
mockUseEvents.mockReturnValue({
|
|
128
|
+
selectedEvent: null,
|
|
129
|
+
events: [{ id: 'event-1', name: 'Test Event' }],
|
|
130
|
+
isLoading: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
renderWithProviders(<ProtectedRoute />);
|
|
134
|
+
|
|
135
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
136
|
+
expect(console.debug).toHaveBeenCalledWith(
|
|
137
|
+
'[ProtectedRoute] Events available but none selected - allowing render so selector is visible'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('renders session restoration loader when session is restoring', () => {
|
|
142
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
143
|
+
isRestoring: true,
|
|
144
|
+
restorationComplete: false,
|
|
145
|
+
restorationError: null,
|
|
146
|
+
hasTimedOut: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
renderWithProviders(<ProtectedRoute />);
|
|
150
|
+
|
|
151
|
+
expect(screen.getByTestId('session-restoration-loader')).toBeInTheDocument();
|
|
152
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
153
|
+
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders loading spinner when auth state is loading and not timed out', () => {
|
|
157
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
158
|
+
isAuthenticated: false,
|
|
159
|
+
authLoading: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
renderWithProviders(<ProtectedRoute />);
|
|
163
|
+
|
|
164
|
+
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
|
165
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
166
|
+
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('renders custom loading fallback when provided', () => {
|
|
170
|
+
const customLoader = <div data-testid="custom-loader">Custom Loading...</div>;
|
|
171
|
+
|
|
172
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
173
|
+
isAuthenticated: false,
|
|
174
|
+
authLoading: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
renderWithProviders(<ProtectedRoute loadingFallback={customLoader} />);
|
|
178
|
+
|
|
179
|
+
expect(screen.getByTestId('custom-loader')).toBeInTheDocument();
|
|
180
|
+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Authentication States', () => {
|
|
185
|
+
it('redirects unauthenticated user to login page', () => {
|
|
186
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
187
|
+
isAuthenticated: false,
|
|
188
|
+
authLoading: false,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
renderWithProviders(<ProtectedRoute />);
|
|
192
|
+
|
|
193
|
+
const navigate = screen.getByTestId('navigate');
|
|
194
|
+
expect(navigate).toBeInTheDocument();
|
|
195
|
+
expect(navigate).toHaveAttribute('data-to', '/login');
|
|
196
|
+
expect(navigate).toHaveAttribute('data-replace', 'true');
|
|
197
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('uses custom login path when provided', () => {
|
|
201
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
202
|
+
isAuthenticated: false,
|
|
203
|
+
authLoading: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
renderWithProviders(<ProtectedRoute loginPath="/custom-login" />);
|
|
207
|
+
|
|
208
|
+
const navigate = screen.getByTestId('navigate');
|
|
209
|
+
expect(navigate).toHaveAttribute('data-to', '/custom-login');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('uses replace navigation to prevent back button from returning to protected route', () => {
|
|
213
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
214
|
+
isAuthenticated: false,
|
|
215
|
+
authLoading: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
renderWithProviders(<ProtectedRoute />);
|
|
219
|
+
|
|
220
|
+
const navigate = screen.getByTestId('navigate');
|
|
221
|
+
expect(navigate).toHaveAttribute('data-replace', 'true');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('warns when redirecting due to session restoration timeout', () => {
|
|
225
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
226
|
+
isAuthenticated: false,
|
|
227
|
+
authLoading: false,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
231
|
+
isRestoring: false,
|
|
232
|
+
restorationComplete: false,
|
|
233
|
+
restorationError: null,
|
|
234
|
+
hasTimedOut: true,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
renderWithProviders(<ProtectedRoute />);
|
|
238
|
+
|
|
239
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
240
|
+
'[ProtectedRoute] Session restoration failed, redirecting to login',
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
timedOut: true,
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('warns when redirecting due to session restoration error', () => {
|
|
248
|
+
const mockError = new Error('Session restoration failed');
|
|
249
|
+
|
|
250
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
251
|
+
isAuthenticated: false,
|
|
252
|
+
authLoading: false,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
256
|
+
isRestoring: false,
|
|
257
|
+
restorationComplete: false,
|
|
258
|
+
restorationError: mockError,
|
|
259
|
+
hasTimedOut: false,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
renderWithProviders(<ProtectedRoute />);
|
|
263
|
+
|
|
264
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
265
|
+
'[ProtectedRoute] Session restoration failed, redirecting to login',
|
|
266
|
+
expect.objectContaining({
|
|
267
|
+
error: 'Session restoration failed',
|
|
268
|
+
timedOut: false,
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('Session Restoration States', () => {
|
|
275
|
+
it('shows restoration loader when restoration is in progress', () => {
|
|
276
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
277
|
+
isRestoring: true,
|
|
278
|
+
restorationComplete: false,
|
|
279
|
+
restorationError: null,
|
|
280
|
+
hasTimedOut: false,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
renderWithProviders(<ProtectedRoute />);
|
|
284
|
+
|
|
285
|
+
expect(screen.getByTestId('session-restoration-loader')).toBeInTheDocument();
|
|
286
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('does not show restoration loader when restoration is complete', () => {
|
|
290
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
291
|
+
isRestoring: false,
|
|
292
|
+
restorationComplete: true,
|
|
293
|
+
restorationError: null,
|
|
294
|
+
hasTimedOut: false,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
renderWithProviders(<ProtectedRoute />);
|
|
298
|
+
|
|
299
|
+
expect(screen.queryByTestId('session-restoration-loader')).not.toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('prioritizes session restoration over loading state', () => {
|
|
303
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
304
|
+
isAuthenticated: false,
|
|
305
|
+
authLoading: true,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
309
|
+
isRestoring: true,
|
|
310
|
+
restorationComplete: false,
|
|
311
|
+
restorationError: null,
|
|
312
|
+
hasTimedOut: false,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
renderWithProviders(<ProtectedRoute />);
|
|
316
|
+
|
|
317
|
+
expect(screen.getByTestId('session-restoration-loader')).toBeInTheDocument();
|
|
318
|
+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('skips loading spinner when session restoration has timed out even if loading', () => {
|
|
322
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
323
|
+
isAuthenticated: false,
|
|
324
|
+
authLoading: true,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
328
|
+
isRestoring: false,
|
|
329
|
+
restorationComplete: false,
|
|
330
|
+
restorationError: null,
|
|
331
|
+
hasTimedOut: true,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
renderWithProviders(<ProtectedRoute />);
|
|
335
|
+
|
|
336
|
+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
|
337
|
+
expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/login');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('Event Management', () => {
|
|
342
|
+
it('allows rendering during event loading when requireEvent is true', () => {
|
|
343
|
+
mockUseEvents.mockReturnValue({
|
|
344
|
+
selectedEvent: null,
|
|
345
|
+
events: [],
|
|
346
|
+
isLoading: true,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
350
|
+
|
|
351
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
352
|
+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('renders outlet when requireEvent is false regardless of event state', () => {
|
|
356
|
+
mockUseEvents.mockReturnValue({
|
|
357
|
+
selectedEvent: null,
|
|
358
|
+
events: [],
|
|
359
|
+
isLoading: false,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
renderWithProviders(<ProtectedRoute requireEvent={false} />);
|
|
363
|
+
|
|
364
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('shows error message when no events are available and requireEvent is true', () => {
|
|
368
|
+
mockUseEvents.mockReturnValue({
|
|
369
|
+
selectedEvent: null,
|
|
370
|
+
events: [],
|
|
371
|
+
isLoading: false,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
375
|
+
|
|
376
|
+
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
377
|
+
expect(screen.getByTestId('alert-title')).toHaveTextContent('No Events Available');
|
|
378
|
+
expect(screen.getByTestId('alert-description')).toHaveTextContent(
|
|
379
|
+
"You don't have access to any events. Please contact your administrator if you believe this is an error."
|
|
380
|
+
);
|
|
381
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('renders custom no events fallback when provided', () => {
|
|
385
|
+
const customFallback = (
|
|
386
|
+
<div data-testid="custom-no-events">Custom No Events Message</div>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
mockUseEvents.mockReturnValue({
|
|
390
|
+
selectedEvent: null,
|
|
391
|
+
events: [],
|
|
392
|
+
isLoading: false,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} noEventsFallback={customFallback} />);
|
|
396
|
+
|
|
397
|
+
expect(screen.getByTestId('custom-no-events')).toBeInTheDocument();
|
|
398
|
+
expect(screen.queryByTestId('alert')).not.toBeInTheDocument();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('allows rendering when events exist but none selected (for event selector visibility)', () => {
|
|
402
|
+
mockUseEvents.mockReturnValue({
|
|
403
|
+
selectedEvent: null,
|
|
404
|
+
events: [
|
|
405
|
+
{ id: 'event-1', name: 'Event 1' },
|
|
406
|
+
{ id: 'event-2', name: 'Event 2' },
|
|
407
|
+
],
|
|
408
|
+
isLoading: false,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
412
|
+
|
|
413
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
414
|
+
expect(console.debug).toHaveBeenCalledWith(
|
|
415
|
+
'[ProtectedRoute] Events available but none selected - allowing render so selector is visible'
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('renders outlet when event is selected', () => {
|
|
420
|
+
mockUseEvents.mockReturnValue({
|
|
421
|
+
selectedEvent: { id: 'event-1', name: 'Event 1' },
|
|
422
|
+
events: [{ id: 'event-1', name: 'Event 1' }],
|
|
423
|
+
isLoading: false,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
427
|
+
|
|
428
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('handles null events array', () => {
|
|
432
|
+
mockUseEvents.mockReturnValue({
|
|
433
|
+
selectedEvent: null,
|
|
434
|
+
events: null as any,
|
|
435
|
+
isLoading: false,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
439
|
+
|
|
440
|
+
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
441
|
+
expect(screen.getByTestId('alert-title')).toHaveTextContent('No Events Available');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('Edge Cases', () => {
|
|
446
|
+
it('handles rapid state changes gracefully', async () => {
|
|
447
|
+
// Start with session restoration
|
|
448
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
449
|
+
isRestoring: true,
|
|
450
|
+
restorationComplete: false,
|
|
451
|
+
restorationError: null,
|
|
452
|
+
hasTimedOut: false,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const { rerender } = renderWithProviders(<ProtectedRoute />);
|
|
456
|
+
|
|
457
|
+
expect(screen.getByTestId('session-restoration-loader')).toBeInTheDocument();
|
|
458
|
+
|
|
459
|
+
// Simulate state change to authenticated
|
|
460
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
461
|
+
isRestoring: false,
|
|
462
|
+
restorationComplete: true,
|
|
463
|
+
restorationError: null,
|
|
464
|
+
hasTimedOut: false,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
rerender(<ProtectedRoute />);
|
|
468
|
+
|
|
469
|
+
await waitFor(
|
|
470
|
+
() => {
|
|
471
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
472
|
+
},
|
|
473
|
+
{ timeout: 2000 }
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(screen.queryByTestId('session-restoration-loader')).not.toBeInTheDocument();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('handles case where restoration completes but user is authenticated', () => {
|
|
480
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
481
|
+
isAuthenticated: true,
|
|
482
|
+
authLoading: false,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
486
|
+
isRestoring: false,
|
|
487
|
+
restorationComplete: true,
|
|
488
|
+
restorationError: null,
|
|
489
|
+
hasTimedOut: false,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
renderWithProviders(<ProtectedRoute />);
|
|
493
|
+
|
|
494
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
495
|
+
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('handles case where restoration times out but user is authenticated', () => {
|
|
499
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
500
|
+
isAuthenticated: true,
|
|
501
|
+
authLoading: false,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
505
|
+
isRestoring: false,
|
|
506
|
+
restorationComplete: false,
|
|
507
|
+
restorationError: null,
|
|
508
|
+
hasTimedOut: true,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
renderWithProviders(<ProtectedRoute />);
|
|
512
|
+
|
|
513
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
514
|
+
expect(screen.queryByTestId('navigate')).not.toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('prioritizes event loading over auth loading when requireEvent is true', () => {
|
|
518
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
519
|
+
isAuthenticated: true,
|
|
520
|
+
authLoading: true,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
mockUseEvents.mockReturnValue({
|
|
524
|
+
selectedEvent: null,
|
|
525
|
+
events: [],
|
|
526
|
+
isLoading: true,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
530
|
+
|
|
531
|
+
// Should render outlet during event loading, not show auth loading spinner
|
|
532
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
533
|
+
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('handles empty events array correctly', () => {
|
|
537
|
+
mockUseEvents.mockReturnValue({
|
|
538
|
+
selectedEvent: null,
|
|
539
|
+
events: [],
|
|
540
|
+
isLoading: false,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
544
|
+
|
|
545
|
+
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
546
|
+
expect(screen.queryByTestId('outlet')).not.toBeInTheDocument();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe('Props Configuration', () => {
|
|
551
|
+
it('defaults requireEvent to true when not provided', () => {
|
|
552
|
+
mockUseEvents.mockReturnValue({
|
|
553
|
+
selectedEvent: null,
|
|
554
|
+
events: [],
|
|
555
|
+
isLoading: false,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
renderWithProviders(<ProtectedRoute />);
|
|
559
|
+
|
|
560
|
+
// Should show no events error since requireEvent defaults to true
|
|
561
|
+
expect(screen.getByTestId('alert')).toBeInTheDocument();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('respects requireEvent prop when set to false', () => {
|
|
565
|
+
mockUseEvents.mockReturnValue({
|
|
566
|
+
selectedEvent: null,
|
|
567
|
+
events: [],
|
|
568
|
+
isLoading: false,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
renderWithProviders(<ProtectedRoute requireEvent={false} />);
|
|
572
|
+
|
|
573
|
+
// Should render outlet even with no events
|
|
574
|
+
expect(screen.getByTestId('outlet')).toBeInTheDocument();
|
|
575
|
+
expect(screen.queryByTestId('alert')).not.toBeInTheDocument();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('defaults loginPath to /login when not provided', () => {
|
|
579
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
580
|
+
isAuthenticated: false,
|
|
581
|
+
authLoading: false,
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
renderWithProviders(<ProtectedRoute />);
|
|
585
|
+
|
|
586
|
+
const navigate = screen.getByTestId('navigate');
|
|
587
|
+
expect(navigate).toHaveAttribute('data-to', '/login');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('uses custom loginPath when provided', () => {
|
|
591
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
592
|
+
isAuthenticated: false,
|
|
593
|
+
authLoading: false,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
renderWithProviders(<ProtectedRoute loginPath="/auth/login" />);
|
|
597
|
+
|
|
598
|
+
const navigate = screen.getByTestId('navigate');
|
|
599
|
+
expect(navigate).toHaveAttribute('data-to', '/auth/login');
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('Accessibility', () => {
|
|
604
|
+
it('provides loading state with screen reader support', () => {
|
|
605
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
606
|
+
isAuthenticated: false,
|
|
607
|
+
authLoading: true,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
renderWithProviders(<ProtectedRoute />);
|
|
611
|
+
|
|
612
|
+
const spinner = screen.getByRole('status');
|
|
613
|
+
expect(spinner).toBeInTheDocument();
|
|
614
|
+
expect(spinner).toHaveTextContent('Loading...');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('provides session restoration state with screen reader support', () => {
|
|
618
|
+
mockUseSessionRestoration.mockReturnValue({
|
|
619
|
+
isRestoring: true,
|
|
620
|
+
restorationComplete: false,
|
|
621
|
+
restorationError: null,
|
|
622
|
+
hasTimedOut: false,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
renderWithProviders(<ProtectedRoute />);
|
|
626
|
+
|
|
627
|
+
const status = screen.getByRole('status');
|
|
628
|
+
expect(status).toBeInTheDocument();
|
|
629
|
+
expect(status).toHaveTextContent('Restoring session...');
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('provides clear error message when no events are available', () => {
|
|
633
|
+
mockUseEvents.mockReturnValue({
|
|
634
|
+
selectedEvent: null,
|
|
635
|
+
events: [],
|
|
636
|
+
isLoading: false,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
renderWithProviders(<ProtectedRoute requireEvent={true} />);
|
|
640
|
+
|
|
641
|
+
expect(screen.getByTestId('alert-title')).toHaveTextContent('No Events Available');
|
|
642
|
+
expect(screen.getByTestId('alert-description')).toHaveTextContent(
|
|
643
|
+
"You don't have access to any events. Please contact your administrator if you believe this is an error."
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
@@ -136,7 +136,7 @@ export function ProtectedRoute({
|
|
|
136
136
|
loadingFallback,
|
|
137
137
|
loginPath = '/login'
|
|
138
138
|
}: ProtectedRouteProps) {
|
|
139
|
-
const { isAuthenticated,
|
|
139
|
+
const { isAuthenticated, authLoading } = useUnifiedAuth();
|
|
140
140
|
const { selectedEvent, events, isLoading: eventLoading } = useEvents();
|
|
141
141
|
const sessionRestoration = useSessionRestoration();
|
|
142
142
|
|
|
@@ -157,8 +157,14 @@ export function ProtectedRoute({
|
|
|
157
157
|
return <SessionRestorationLoader />;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
161
|
-
|
|
160
|
+
// Allow rendering during event loading - prevents blocking UI while events load
|
|
161
|
+
// This must come before auth loading check to avoid blocking when only events are loading
|
|
162
|
+
if (requireEvent && eventLoading) {
|
|
163
|
+
return <Outlet />;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Show loading state while auth is being determined (but not organisation/event loading)
|
|
167
|
+
if (authLoading && !sessionRestoration.hasTimedOut) {
|
|
162
168
|
return loadingFallback || (
|
|
163
169
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
164
170
|
<LoadingSpinner />
|
|
@@ -186,10 +192,7 @@ export function ProtectedRoute({
|
|
|
186
192
|
// Apps that need super admin access without events should set requireEvent={false}
|
|
187
193
|
// For now, we keep it simple and always require events when requireEvent=true
|
|
188
194
|
|
|
189
|
-
//
|
|
190
|
-
if (eventLoading) {
|
|
191
|
-
return <Outlet />;
|
|
192
|
-
}
|
|
195
|
+
// Event loading check already handled above before auth loading check
|
|
193
196
|
|
|
194
197
|
// If no events are available, show error message
|
|
195
198
|
if (!events || events.length === 0) {
|