@jmruthers/pace-core 0.5.1 → 0.5.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/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -58
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- 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 +96 -0
- 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 +235 -0
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- 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 +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- 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 +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- 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/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
- 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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +441 -0
- package/src/rbac/hooks/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +359 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +503 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -58
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file PaceLoginPage Component Tests
|
|
3
|
+
* @description Comprehensive tests for PaceLoginPage component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { PaceLoginPage } from './PaceLoginPage';
|
|
11
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
12
|
+
|
|
13
|
+
// Mock React Router
|
|
14
|
+
const mockNavigate = vi.fn();
|
|
15
|
+
vi.mock('react-router-dom', () => ({
|
|
16
|
+
useNavigate: () => mockNavigate,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Mock the UnifiedAuthProvider
|
|
20
|
+
const mockAuthContext = {
|
|
21
|
+
user: null,
|
|
22
|
+
isAuthenticated: false,
|
|
23
|
+
isLoading: false,
|
|
24
|
+
authError: null,
|
|
25
|
+
hasRole: vi.fn(),
|
|
26
|
+
signIn: vi.fn(),
|
|
27
|
+
signOut: vi.fn(),
|
|
28
|
+
refreshSession: vi.fn(),
|
|
29
|
+
appName: 'Test App',
|
|
30
|
+
hasErrors: false,
|
|
31
|
+
// RBAC context
|
|
32
|
+
globalRole: null,
|
|
33
|
+
organisationRole: null,
|
|
34
|
+
eventAppRole: null,
|
|
35
|
+
rbacLoading: false,
|
|
36
|
+
rbacError: null,
|
|
37
|
+
// Inactivity context
|
|
38
|
+
isIdle: false,
|
|
39
|
+
timeUntilIdle: 0,
|
|
40
|
+
resetInactivityTimer: vi.fn(),
|
|
41
|
+
inactivityLoading: false,
|
|
42
|
+
inactivityError: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Mock the useUnifiedAuth hook
|
|
46
|
+
vi.mock('../../providers/UnifiedAuthProvider', () => ({
|
|
47
|
+
useUnifiedAuth: () => mockAuthContext,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Mock console methods to avoid noise in tests
|
|
51
|
+
const originalConsoleError = console.error;
|
|
52
|
+
|
|
53
|
+
describe('PaceLoginPage Component', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
// Reset console mocks
|
|
57
|
+
console.error = vi.fn();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
// Restore console methods
|
|
62
|
+
console.error = originalConsoleError;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Basic rendering tests
|
|
66
|
+
describe('Rendering', () => {
|
|
67
|
+
it('renders with default props', () => {
|
|
68
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders with custom app name', () => {
|
|
74
|
+
renderWithProviders(<PaceLoginPage appName="My Application" />);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByLabelText('My Application Login Page')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders with custom redirect path', () => {
|
|
80
|
+
renderWithProviders(
|
|
81
|
+
<PaceLoginPage
|
|
82
|
+
appName="Test App"
|
|
83
|
+
onSuccessRedirectPath="/dashboard"
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders app logo with correct attributes', () => {
|
|
91
|
+
renderWithProviders(<PaceLoginPage appName="TestApp" />);
|
|
92
|
+
|
|
93
|
+
const logo = screen.getByAltText('TestApp logo');
|
|
94
|
+
expect(logo).toBeInTheDocument();
|
|
95
|
+
expect(logo).toHaveAttribute('src', '/testapp_logo_square.svg');
|
|
96
|
+
expect(logo).toHaveClass('h-48');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders LoginForm component', () => {
|
|
100
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
101
|
+
|
|
102
|
+
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('passes correct props to LoginForm', () => {
|
|
106
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
107
|
+
|
|
108
|
+
// Check that LoginForm receives the app name
|
|
109
|
+
expect(screen.getByText('Sign in to Test App')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Authentication state tests
|
|
114
|
+
describe('Authentication State', () => {
|
|
115
|
+
it('handles loading state', () => {
|
|
116
|
+
mockAuthContext.isLoading = true;
|
|
117
|
+
|
|
118
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
119
|
+
|
|
120
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('handles authenticated state', () => {
|
|
124
|
+
mockAuthContext.isAuthenticated = true;
|
|
125
|
+
mockAuthContext.isLoading = false;
|
|
126
|
+
mockAuthContext.hasRole.mockReturnValue(false);
|
|
127
|
+
|
|
128
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('handles authentication error', () => {
|
|
134
|
+
mockAuthContext.authError = new Error('Authentication failed');
|
|
135
|
+
|
|
136
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByText('Authentication failed')).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByText('Authentication failed')).toHaveClass('text-destructive');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('displays auth error with proper styling', () => {
|
|
143
|
+
const errorMessage = 'Invalid credentials';
|
|
144
|
+
mockAuthContext.authError = new Error(errorMessage);
|
|
145
|
+
|
|
146
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
147
|
+
|
|
148
|
+
const errorElement = screen.getByText(errorMessage);
|
|
149
|
+
expect(errorElement).toBeInTheDocument();
|
|
150
|
+
expect(errorElement).toHaveClass('mt-4', 'text-destructive', 'text-center');
|
|
151
|
+
expect(errorElement.tagName).toBe('EM');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Role-based redirection tests
|
|
156
|
+
describe('Role-Based Redirection', () => {
|
|
157
|
+
it('redirects super admin users automatically', async () => {
|
|
158
|
+
mockAuthContext.isAuthenticated = true;
|
|
159
|
+
mockAuthContext.isLoading = false;
|
|
160
|
+
mockAuthContext.hasRole.mockReturnValue(true);
|
|
161
|
+
|
|
162
|
+
renderWithProviders(
|
|
163
|
+
<PaceLoginPage
|
|
164
|
+
appName="Test App"
|
|
165
|
+
onSuccessRedirectPath="/admin"
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(mockNavigate).toHaveBeenCalledWith('/admin', { replace: true });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('does not redirect non-super admin users automatically', async () => {
|
|
175
|
+
mockAuthContext.isAuthenticated = true;
|
|
176
|
+
mockAuthContext.isLoading = false;
|
|
177
|
+
mockAuthContext.hasRole.mockReturnValue(false);
|
|
178
|
+
|
|
179
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
180
|
+
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('handles navigation errors gracefully', async () => {
|
|
187
|
+
mockAuthContext.isAuthenticated = true;
|
|
188
|
+
mockAuthContext.isLoading = false;
|
|
189
|
+
mockAuthContext.hasRole.mockReturnValue(true);
|
|
190
|
+
mockNavigate.mockImplementation(() => {
|
|
191
|
+
throw new Error('Navigation failed');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
195
|
+
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(console.error).toHaveBeenCalledWith('Navigation error:', expect.any(Error));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Form submission tests
|
|
203
|
+
describe('Form Submission', () => {
|
|
204
|
+
it('handles successful login submission', async () => {
|
|
205
|
+
const user = userEvent.setup();
|
|
206
|
+
mockAuthContext.signIn.mockResolvedValue({ error: null });
|
|
207
|
+
|
|
208
|
+
renderWithProviders(
|
|
209
|
+
<PaceLoginPage
|
|
210
|
+
appName="Test App"
|
|
211
|
+
onSuccessRedirectPath="/dashboard"
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const emailInput = screen.getByLabelText('Email');
|
|
216
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
217
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
218
|
+
|
|
219
|
+
await user.type(emailInput, 'test@example.com');
|
|
220
|
+
await user.type(passwordInput, 'password123');
|
|
221
|
+
await user.click(submitButton);
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(mockAuthContext.signIn).toHaveBeenCalledWith('test@example.com', 'password123');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('handles failed login submission', async () => {
|
|
233
|
+
const user = userEvent.setup();
|
|
234
|
+
const signInError = new Error('Invalid credentials');
|
|
235
|
+
mockAuthContext.signIn.mockResolvedValue({ error: signInError });
|
|
236
|
+
|
|
237
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
238
|
+
|
|
239
|
+
const emailInput = screen.getByLabelText('Email');
|
|
240
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
241
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
242
|
+
|
|
243
|
+
await user.type(emailInput, 'test@example.com');
|
|
244
|
+
await user.type(passwordInput, 'wrongpassword');
|
|
245
|
+
await user.click(submitButton);
|
|
246
|
+
|
|
247
|
+
await waitFor(() => {
|
|
248
|
+
expect(mockAuthContext.signIn).toHaveBeenCalledWith('test@example.com', 'wrongpassword');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Should not navigate on error - the component will still try to navigate
|
|
252
|
+
// but we can check that the error is displayed
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(screen.getByText('Invalid credentials')).toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('handles navigation error after successful login', async () => {
|
|
259
|
+
const user = userEvent.setup();
|
|
260
|
+
mockAuthContext.signIn.mockResolvedValue({ error: null });
|
|
261
|
+
mockNavigate.mockImplementation(() => {
|
|
262
|
+
throw new Error('Navigation failed');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
266
|
+
|
|
267
|
+
const emailInput = screen.getByLabelText('Email');
|
|
268
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
269
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
270
|
+
|
|
271
|
+
await user.type(emailInput, 'test@example.com');
|
|
272
|
+
await user.type(passwordInput, 'password123');
|
|
273
|
+
await user.click(submitButton);
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(console.error).toHaveBeenCalledWith('Navigation error after sign-in:', expect.any(Error));
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('manages loading state during form submission', async () => {
|
|
281
|
+
const user = userEvent.setup();
|
|
282
|
+
let resolveSignIn: (value: any) => void;
|
|
283
|
+
const signInPromise = new Promise((resolve) => {
|
|
284
|
+
resolveSignIn = resolve;
|
|
285
|
+
});
|
|
286
|
+
mockAuthContext.signIn.mockReturnValue(signInPromise);
|
|
287
|
+
|
|
288
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
289
|
+
|
|
290
|
+
const emailInput = screen.getByLabelText('Email');
|
|
291
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
292
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
293
|
+
|
|
294
|
+
await user.type(emailInput, 'test@example.com');
|
|
295
|
+
await user.type(passwordInput, 'password123');
|
|
296
|
+
await user.click(submitButton);
|
|
297
|
+
|
|
298
|
+
// Form should be in loading state
|
|
299
|
+
expect(emailInput).toBeDisabled();
|
|
300
|
+
expect(passwordInput).toBeDisabled();
|
|
301
|
+
expect(submitButton).toBeDisabled();
|
|
302
|
+
|
|
303
|
+
// Resolve the sign-in promise
|
|
304
|
+
resolveSignIn!({ error: null });
|
|
305
|
+
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(emailInput).not.toBeDisabled();
|
|
308
|
+
expect(passwordInput).not.toBeDisabled();
|
|
309
|
+
expect(submitButton).not.toBeDisabled();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Error handling tests
|
|
315
|
+
describe('Error Handling', () => {
|
|
316
|
+
it('handles missing app name gracefully', () => {
|
|
317
|
+
renderWithProviders(<PaceLoginPage appName="" />);
|
|
318
|
+
|
|
319
|
+
// Check that the component renders even with empty app name
|
|
320
|
+
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('handles undefined redirect path', () => {
|
|
324
|
+
renderWithProviders(<PaceLoginPage appName="Test App" onSuccessRedirectPath={undefined} />);
|
|
325
|
+
|
|
326
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('handles sign-in function errors', async () => {
|
|
330
|
+
const user = userEvent.setup();
|
|
331
|
+
mockAuthContext.signIn.mockRejectedValue(new Error('Network error'));
|
|
332
|
+
|
|
333
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
334
|
+
|
|
335
|
+
const emailInput = screen.getByLabelText('Email');
|
|
336
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
337
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
338
|
+
|
|
339
|
+
await user.type(emailInput, 'test@example.com');
|
|
340
|
+
await user.type(passwordInput, 'password123');
|
|
341
|
+
await user.click(submitButton);
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(console.error).toHaveBeenCalledWith('Login error:', expect.any(Error));
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Accessibility tests
|
|
350
|
+
describe('Accessibility', () => {
|
|
351
|
+
it('has proper ARIA attributes', () => {
|
|
352
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
353
|
+
|
|
354
|
+
const main = screen.getByLabelText('Test App Login Page');
|
|
355
|
+
expect(main).toHaveAttribute('aria-label', 'Test App Login Page');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('has proper semantic structure', () => {
|
|
359
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
360
|
+
|
|
361
|
+
expect(screen.getByLabelText('Test App Login Page')).toBeInTheDocument();
|
|
362
|
+
expect(screen.getByRole('img')).toBeInTheDocument();
|
|
363
|
+
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('has accessible form elements', () => {
|
|
367
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
368
|
+
|
|
369
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
370
|
+
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
|
371
|
+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('announces errors to screen readers', () => {
|
|
375
|
+
mockAuthContext.authError = new Error('Authentication failed');
|
|
376
|
+
|
|
377
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
378
|
+
|
|
379
|
+
const errorElement = screen.getByText('Authentication failed');
|
|
380
|
+
expect(errorElement).toBeInTheDocument();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Integration tests
|
|
385
|
+
describe('Integration', () => {
|
|
386
|
+
it('integrates with LoginForm component', () => {
|
|
387
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
388
|
+
|
|
389
|
+
// Check that LoginForm is rendered with correct props
|
|
390
|
+
expect(screen.getByTestId('login-form')).toBeInTheDocument();
|
|
391
|
+
expect(screen.getByText('Sign in to Test App')).toBeInTheDocument();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('passes loading state to LoginForm', async () => {
|
|
395
|
+
const user = userEvent.setup();
|
|
396
|
+
let resolveSignIn: (value: any) => void;
|
|
397
|
+
const signInPromise = new Promise((resolve) => {
|
|
398
|
+
resolveSignIn = resolve;
|
|
399
|
+
});
|
|
400
|
+
mockAuthContext.signIn.mockReturnValue(signInPromise);
|
|
401
|
+
|
|
402
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
403
|
+
|
|
404
|
+
const emailInput = screen.getByLabelText('Email');
|
|
405
|
+
const passwordInput = screen.getByLabelText('Password');
|
|
406
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
407
|
+
|
|
408
|
+
await user.type(emailInput, 'test@example.com');
|
|
409
|
+
await user.type(passwordInput, 'password123');
|
|
410
|
+
await user.click(submitButton);
|
|
411
|
+
|
|
412
|
+
// LoginForm should be in loading state
|
|
413
|
+
expect(emailInput).toBeDisabled();
|
|
414
|
+
expect(passwordInput).toBeDisabled();
|
|
415
|
+
expect(submitButton).toBeDisabled();
|
|
416
|
+
|
|
417
|
+
resolveSignIn!({ error: null });
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Layout and styling tests
|
|
422
|
+
describe('Layout and Styling', () => {
|
|
423
|
+
it('has correct main container classes', () => {
|
|
424
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
425
|
+
|
|
426
|
+
const main = screen.getByLabelText('Test App Login Page');
|
|
427
|
+
expect(main).toHaveClass('min-h-screen', 'grid', 'mx-auto', 'w-fit', 'content-center', 'justify-items-center', 'gap-y-8');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('renders logo with correct styling', () => {
|
|
431
|
+
renderWithProviders(<PaceLoginPage appName="TestApp" />);
|
|
432
|
+
|
|
433
|
+
const logo = screen.getByAltText('TestApp logo');
|
|
434
|
+
expect(logo).toHaveClass('h-48');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('renders LoginForm with correct width class', () => {
|
|
438
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
439
|
+
|
|
440
|
+
const form = screen.getByTestId('login-form');
|
|
441
|
+
expect(form.closest('.w-md')).toBeInTheDocument();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Edge cases tests
|
|
446
|
+
describe('Edge Cases', () => {
|
|
447
|
+
it('handles empty form submission', async () => {
|
|
448
|
+
const user = userEvent.setup();
|
|
449
|
+
|
|
450
|
+
renderWithProviders(<PaceLoginPage appName="Test App" />);
|
|
451
|
+
|
|
452
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
453
|
+
await user.click(submitButton);
|
|
454
|
+
|
|
455
|
+
// Should not call signIn with empty data
|
|
456
|
+
expect(mockAuthContext.signIn).not.toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('handles very long app names', () => {
|
|
460
|
+
const longAppName = 'A'.repeat(100);
|
|
461
|
+
|
|
462
|
+
renderWithProviders(<PaceLoginPage appName={longAppName} />);
|
|
463
|
+
|
|
464
|
+
expect(screen.getByLabelText(`${longAppName} Login Page`)).toBeInTheDocument();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('handles special characters in app name', () => {
|
|
468
|
+
const specialAppName = 'Test & Co. (Ltd.)';
|
|
469
|
+
|
|
470
|
+
renderWithProviders(<PaceLoginPage appName={specialAppName} />);
|
|
471
|
+
|
|
472
|
+
expect(screen.getByLabelText(`${specialAppName} Login Page`)).toBeInTheDocument();
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|