@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.
Files changed (206) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -58
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +441 -0
  174. package/src/rbac/hooks/index.ts +21 -0
  175. package/src/rbac/hooks/useCan.test.ts +461 -0
  176. package/src/rbac/hooks/usePermissions.test.ts +359 -0
  177. package/src/rbac/hooks/usePermissions.ts +567 -0
  178. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  179. package/src/rbac/hooks/useRBAC.test.ts +503 -0
  180. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  181. package/src/rbac/index.ts +5 -10
  182. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  183. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  184. package/src/rbac/providers/index.ts +11 -0
  185. package/src/styles/core.css +0 -58
  186. package/src/utils/formatDate.test.ts +241 -0
  187. package/dist/chunk-AUE24LVR.js +0 -268
  188. package/dist/chunk-AUE24LVR.js.map +0 -1
  189. package/dist/chunk-COBPIXXQ.js +0 -379
  190. package/dist/chunk-COBPIXXQ.js.map +0 -1
  191. package/dist/chunk-OEGRKULD.js.map +0 -1
  192. package/dist/chunk-OYRY44Q2.js +0 -62
  193. package/dist/chunk-OYRY44Q2.js.map +0 -1
  194. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  195. package/dist/chunk-TGDCLPP2.js.map +0 -1
  196. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  197. package/src/components/RBAC/RBACGuard.tsx +0 -143
  198. package/src/components/RBAC/RBACProvider.tsx +0 -186
  199. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  200. package/src/components/RBAC/index.ts +0 -23
  201. package/src/rbac/hooks.ts +0 -570
  202. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  203. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  204. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  205. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  206. /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
+ });