@jmruthers/pace-core 0.5.1 → 0.5.4

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 (210) 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 -55
  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 +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
@@ -0,0 +1,816 @@
1
+ /**
2
+ * @file LoginForm Component Tests
3
+ * @description Comprehensive test suite for LoginForm component
4
+ * @package @jmruthers/pace-core
5
+ */
6
+
7
+ import React from 'react';
8
+ import { screen, waitFor } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ import { LoginForm } from './LoginForm';
12
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
13
+
14
+ // Mock the Card components
15
+ vi.mock('../Card/index', () => ({
16
+ Card: ({ children, className, ...props }: any) => (
17
+ <div data-testid="card" className={className} {...props}>
18
+ {children}
19
+ </div>
20
+ ),
21
+ CardContent: ({ children, className, ...props }: any) => (
22
+ <div data-testid="card-content" className={className} {...props}>
23
+ {children}
24
+ </div>
25
+ ),
26
+ CardDescription: ({ children, className, ...props }: any) => (
27
+ <p data-testid="card-description" className={className} {...props}>
28
+ {children}
29
+ </p>
30
+ ),
31
+ CardFooter: ({ children, className, ...props }: any) => (
32
+ <div data-testid="card-footer" className={className} {...props}>
33
+ {children}
34
+ </div>
35
+ ),
36
+ CardHeader: ({ children, className, ...props }: any) => (
37
+ <div data-testid="card-header" className={className} {...props}>
38
+ {children}
39
+ </div>
40
+ ),
41
+ CardTitle: ({ children, className, ...props }: any) => (
42
+ <h2 data-testid="card-title" className={className} {...props}>
43
+ {children}
44
+ </h2>
45
+ ),
46
+ }));
47
+
48
+ // Mock the Button component
49
+ vi.mock('../Button/Button', () => ({
50
+ Button: ({ children, onClick, className, disabled, type, ...props }: any) => (
51
+ <button
52
+ onClick={onClick}
53
+ className={className}
54
+ disabled={disabled}
55
+ type={type}
56
+ {...props}
57
+ >
58
+ {children}
59
+ </button>
60
+ ),
61
+ }));
62
+
63
+ // Mock the Input component
64
+ vi.mock('../Input/Input', () => ({
65
+ Input: ({ id, type, placeholder, value, onChange, required, disabled, ...props }: any) => (
66
+ <input
67
+ id={id}
68
+ type={type}
69
+ placeholder={placeholder}
70
+ value={value}
71
+ onChange={onChange}
72
+ required={required}
73
+ disabled={disabled}
74
+ {...props}
75
+ />
76
+ ),
77
+ }));
78
+
79
+ // Mock the Label component
80
+ vi.mock('../Label/Label', () => ({
81
+ Label: ({ children, htmlFor, ...props }: any) => (
82
+ <label htmlFor={htmlFor} {...props}>
83
+ {children}
84
+ </label>
85
+ ),
86
+ }));
87
+
88
+ // Mock the Alert component
89
+ vi.mock('../Alert/Alert', () => ({
90
+ Alert: ({ children, variant, ...props }: any) => (
91
+ <div data-testid="alert" data-variant={variant} {...props}>
92
+ {children}
93
+ </div>
94
+ ),
95
+ AlertDescription: ({ children, ...props }: any) => (
96
+ <div data-testid="alert-description" {...props}>
97
+ {children}
98
+ </div>
99
+ ),
100
+ }));
101
+
102
+ // Mock the cn utility
103
+ vi.mock('../../utils/cn', () => ({
104
+ cn: (...classes: any[]) => classes.filter(Boolean).join(' '),
105
+ }));
106
+
107
+ describe('LoginForm Component', () => {
108
+ const defaultProps = {
109
+ onSignIn: vi.fn(),
110
+ };
111
+
112
+ beforeEach(() => {
113
+ vi.clearAllMocks();
114
+ });
115
+
116
+ describe('Rendering', () => {
117
+ it('renders with default props', () => {
118
+ renderWithProviders(<LoginForm {...defaultProps} />);
119
+
120
+ expect(screen.getByTestId('login-form')).toBeInTheDocument();
121
+ expect(screen.getByTestId('card')).toBeInTheDocument();
122
+ expect(screen.getByTestId('login-form')).toHaveAttribute('data-testid', 'login-form');
123
+ });
124
+
125
+ it('renders with custom title and subtitle', () => {
126
+ renderWithProviders(
127
+ <LoginForm
128
+ {...defaultProps}
129
+ title="Welcome Back"
130
+ subtitle="Please enter your credentials"
131
+ />
132
+ );
133
+
134
+ expect(screen.getByText('Welcome Back')).toBeInTheDocument();
135
+ expect(screen.getByText('Please enter your credentials')).toBeInTheDocument();
136
+ });
137
+
138
+ it('renders with app name in title', () => {
139
+ renderWithProviders(
140
+ <LoginForm
141
+ {...defaultProps}
142
+ appName="My App"
143
+ />
144
+ );
145
+
146
+ expect(screen.getByText('Sign in to My App')).toBeInTheDocument();
147
+ });
148
+
149
+ it('renders with custom className', () => {
150
+ renderWithProviders(
151
+ <LoginForm
152
+ {...defaultProps}
153
+ className="custom-login-form"
154
+ />
155
+ );
156
+
157
+ expect(screen.getByTestId('card')).toHaveClass('custom-login-form');
158
+ });
159
+
160
+ it('renders form inputs with correct attributes', () => {
161
+ renderWithProviders(<LoginForm {...defaultProps} />);
162
+
163
+ const emailInput = screen.getByLabelText('Email');
164
+ const passwordInput = screen.getByLabelText('Password');
165
+
166
+ expect(emailInput).toHaveAttribute('type', 'email');
167
+ expect(emailInput).toHaveAttribute('placeholder', 'Enter your email');
168
+ expect(emailInput).toHaveAttribute('required');
169
+
170
+ expect(passwordInput).toHaveAttribute('type', 'password');
171
+ expect(passwordInput).toHaveAttribute('placeholder', 'Enter your password');
172
+ expect(passwordInput).toHaveAttribute('required');
173
+ });
174
+
175
+ it('renders submit button with correct text', () => {
176
+ renderWithProviders(<LoginForm {...defaultProps} />);
177
+
178
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
179
+ });
180
+
181
+ it('renders sign-up link when showSignUp is true without onSignUp', () => {
182
+ renderWithProviders(<LoginForm {...defaultProps} showSignUp={true} />);
183
+
184
+ expect(screen.getByText("Don't have an account?")).toBeInTheDocument();
185
+ expect(screen.getByRole('link', { name: 'Sign up' })).toBeInTheDocument();
186
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/signup');
187
+ });
188
+
189
+ it('renders sign-up button when showSignUp is true with onSignUp', () => {
190
+ const onSignUp = vi.fn();
191
+ renderWithProviders(
192
+ <LoginForm
193
+ {...defaultProps}
194
+ showSignUp={true}
195
+ onSignUp={onSignUp}
196
+ />
197
+ );
198
+
199
+ expect(screen.getByText("Don't have an account? Sign up")).toBeInTheDocument();
200
+ expect(screen.getByRole('button', { name: "Don't have an account? Sign up" })).toBeInTheDocument();
201
+ });
202
+ });
203
+
204
+ describe('Form Validation', () => {
205
+ it('disables submit button when form is empty', () => {
206
+ renderWithProviders(<LoginForm {...defaultProps} />);
207
+
208
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
209
+ expect(submitButton).toBeDisabled();
210
+ });
211
+
212
+ it('enables submit button when both fields have values', async () => {
213
+ const user = userEvent.setup();
214
+ renderWithProviders(<LoginForm {...defaultProps} />);
215
+
216
+ const emailInput = screen.getByLabelText('Email');
217
+ const passwordInput = screen.getByLabelText('Password');
218
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
219
+
220
+ expect(submitButton).toBeDisabled();
221
+
222
+ await user.type(emailInput, 'test@example.com');
223
+ expect(submitButton).toBeDisabled();
224
+
225
+ await user.type(passwordInput, 'password123');
226
+ expect(submitButton).not.toBeDisabled();
227
+ });
228
+
229
+ it('disables submit button when only email is filled', async () => {
230
+ const user = userEvent.setup();
231
+ renderWithProviders(<LoginForm {...defaultProps} />);
232
+
233
+ const emailInput = screen.getByLabelText('Email');
234
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
235
+
236
+ await user.type(emailInput, 'test@example.com');
237
+ expect(submitButton).toBeDisabled();
238
+ });
239
+
240
+ it('disables submit button when only password is filled', async () => {
241
+ const user = userEvent.setup();
242
+ renderWithProviders(<LoginForm {...defaultProps} />);
243
+
244
+ const passwordInput = screen.getByLabelText('Password');
245
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
246
+
247
+ await user.type(passwordInput, 'password123');
248
+ expect(submitButton).toBeDisabled();
249
+ });
250
+ });
251
+
252
+ describe('User Interactions', () => {
253
+ it('updates email input value when typed', async () => {
254
+ const user = userEvent.setup();
255
+ renderWithProviders(<LoginForm {...defaultProps} />);
256
+
257
+ const emailInput = screen.getByLabelText('Email');
258
+ await user.type(emailInput, 'test@example.com');
259
+
260
+ expect(emailInput).toHaveValue('test@example.com');
261
+ });
262
+
263
+ it('updates password input value when typed', async () => {
264
+ const user = userEvent.setup();
265
+ renderWithProviders(<LoginForm {...defaultProps} />);
266
+
267
+ const passwordInput = screen.getByLabelText('Password');
268
+ await user.type(passwordInput, 'password123');
269
+
270
+ expect(passwordInput).toHaveValue('password123');
271
+ });
272
+
273
+ it('calls onSignIn with form data when submitted', async () => {
274
+ const user = userEvent.setup();
275
+ const onSignIn = vi.fn().mockResolvedValue(undefined);
276
+ renderWithProviders(<LoginForm {...defaultProps} onSignIn={onSignIn} />);
277
+
278
+ const emailInput = screen.getByLabelText('Email');
279
+ const passwordInput = screen.getByLabelText('Password');
280
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
281
+
282
+ await user.type(emailInput, 'test@example.com');
283
+ await user.type(passwordInput, 'password123');
284
+ await user.click(submitButton);
285
+
286
+ expect(onSignIn).toHaveBeenCalledWith({
287
+ email: 'test@example.com',
288
+ password: 'password123'
289
+ });
290
+ });
291
+
292
+ it('calls onSignUp when sign-up button is clicked', async () => {
293
+ const user = userEvent.setup();
294
+ const onSignUp = vi.fn();
295
+ renderWithProviders(
296
+ <LoginForm
297
+ {...defaultProps}
298
+ showSignUp={true}
299
+ onSignUp={onSignUp}
300
+ />
301
+ );
302
+
303
+ const signUpButton = screen.getByRole('button', { name: "Don't have an account? Sign up" });
304
+ await user.click(signUpButton);
305
+
306
+ expect(onSignUp).toHaveBeenCalledTimes(1);
307
+ });
308
+
309
+ it('prevents form submission when form is invalid', async () => {
310
+ const user = userEvent.setup();
311
+ const onSignIn = vi.fn();
312
+ renderWithProviders(<LoginForm {...defaultProps} onSignIn={onSignIn} />);
313
+
314
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
315
+ await user.click(submitButton);
316
+
317
+ expect(onSignIn).not.toHaveBeenCalled();
318
+ });
319
+
320
+ it('prevents form submission when loading', async () => {
321
+ const user = userEvent.setup();
322
+ const onSignIn = vi.fn();
323
+ renderWithProviders(
324
+ <LoginForm
325
+ {...defaultProps}
326
+ onSignIn={onSignIn}
327
+ isLoading={true}
328
+ />
329
+ );
330
+
331
+ const emailInput = screen.getByLabelText('Email');
332
+ const passwordInput = screen.getByLabelText('Password');
333
+ const submitButton = screen.getByRole('button', { name: 'Signing in...' });
334
+
335
+ await user.type(emailInput, 'test@example.com');
336
+ await user.type(passwordInput, 'password123');
337
+ await user.click(submitButton);
338
+
339
+ expect(onSignIn).not.toHaveBeenCalled();
340
+ });
341
+ });
342
+
343
+ describe('Loading States', () => {
344
+ it('shows loading text on submit button when loading', () => {
345
+ renderWithProviders(<LoginForm {...defaultProps} isLoading={true} />);
346
+
347
+ expect(screen.getByRole('button', { name: 'Signing in...' })).toBeInTheDocument();
348
+ });
349
+
350
+ it('disables form inputs when loading', () => {
351
+ renderWithProviders(<LoginForm {...defaultProps} isLoading={true} />);
352
+
353
+ const emailInput = screen.getByLabelText('Email');
354
+ const passwordInput = screen.getByLabelText('Password');
355
+
356
+ expect(emailInput).toBeDisabled();
357
+ expect(passwordInput).toBeDisabled();
358
+ });
359
+
360
+ it('disables submit button when loading', () => {
361
+ renderWithProviders(<LoginForm {...defaultProps} isLoading={true} />);
362
+
363
+ const submitButton = screen.getByRole('button', { name: 'Signing in...' });
364
+ expect(submitButton).toBeDisabled();
365
+ });
366
+ });
367
+
368
+ describe('Error Handling', () => {
369
+ it('displays error message when provided', () => {
370
+ renderWithProviders(<LoginForm {...defaultProps} />);
371
+
372
+ // Simulate error state by triggering form submission with error
373
+ const form = screen.getByTestId('login-form');
374
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
375
+ form.dispatchEvent(submitEvent);
376
+
377
+ // Error should not be visible initially
378
+ expect(screen.queryByTestId('alert')).not.toBeInTheDocument();
379
+ });
380
+
381
+ it('calls onError when sign-in fails', async () => {
382
+ const user = userEvent.setup();
383
+ const onError = vi.fn();
384
+ const onSignIn = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
385
+
386
+ renderWithProviders(
387
+ <LoginForm
388
+ {...defaultProps}
389
+ onSignIn={onSignIn}
390
+ onError={onError}
391
+ />
392
+ );
393
+
394
+ const emailInput = screen.getByLabelText('Email');
395
+ const passwordInput = screen.getByLabelText('Password');
396
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
397
+
398
+ await user.type(emailInput, 'test@example.com');
399
+ await user.type(passwordInput, 'password123');
400
+ await user.click(submitButton);
401
+
402
+ await waitFor(() => {
403
+ expect(onError).toHaveBeenCalledWith(expect.any(Error));
404
+ });
405
+ });
406
+
407
+ it('displays error message when sign-in fails', async () => {
408
+ const user = userEvent.setup();
409
+ const onSignIn = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
410
+
411
+ renderWithProviders(
412
+ <LoginForm
413
+ {...defaultProps}
414
+ onSignIn={onSignIn}
415
+ />
416
+ );
417
+
418
+ const emailInput = screen.getByLabelText('Email');
419
+ const passwordInput = screen.getByLabelText('Password');
420
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
421
+
422
+ await user.type(emailInput, 'test@example.com');
423
+ await user.type(passwordInput, 'password123');
424
+ await user.click(submitButton);
425
+
426
+ await waitFor(() => {
427
+ expect(screen.getByText('Invalid credentials')).toBeInTheDocument();
428
+ });
429
+ });
430
+
431
+ it('handles non-Error objects in catch block', async () => {
432
+ const user = userEvent.setup();
433
+ const onError = vi.fn();
434
+ const onSignIn = vi.fn().mockRejectedValue('String error');
435
+
436
+ renderWithProviders(
437
+ <LoginForm
438
+ {...defaultProps}
439
+ onSignIn={onSignIn}
440
+ onError={onError}
441
+ />
442
+ );
443
+
444
+ const emailInput = screen.getByLabelText('Email');
445
+ const passwordInput = screen.getByLabelText('Password');
446
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
447
+
448
+ await user.type(emailInput, 'test@example.com');
449
+ await user.type(passwordInput, 'password123');
450
+ await user.click(submitButton);
451
+
452
+ await waitFor(() => {
453
+ expect(screen.getByText('Sign in failed')).toBeInTheDocument();
454
+ expect(onError).toHaveBeenCalledWith(expect.any(Error));
455
+ });
456
+ });
457
+
458
+ it('clears error when form is resubmitted', async () => {
459
+ const user = userEvent.setup();
460
+ const onSignIn = vi.fn()
461
+ .mockRejectedValueOnce(new Error('First error'))
462
+ .mockResolvedValueOnce(undefined);
463
+
464
+ renderWithProviders(
465
+ <LoginForm
466
+ {...defaultProps}
467
+ onSignIn={onSignIn}
468
+ />
469
+ );
470
+
471
+ const emailInput = screen.getByLabelText('Email');
472
+ const passwordInput = screen.getByLabelText('Password');
473
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
474
+
475
+ await user.type(emailInput, 'test@example.com');
476
+ await user.type(passwordInput, 'password123');
477
+
478
+ // First submission - should show error
479
+ await user.click(submitButton);
480
+ await waitFor(() => {
481
+ expect(screen.getByText('First error')).toBeInTheDocument();
482
+ });
483
+
484
+ // Second submission - should clear error
485
+ await user.click(submitButton);
486
+ await waitFor(() => {
487
+ expect(screen.queryByText('First error')).not.toBeInTheDocument();
488
+ });
489
+ });
490
+ });
491
+
492
+ describe('Success Handling', () => {
493
+ it('calls onSuccess when sign-in succeeds', async () => {
494
+ const user = userEvent.setup();
495
+ const onSuccess = vi.fn();
496
+ const onSignIn = vi.fn().mockResolvedValue(undefined);
497
+
498
+ renderWithProviders(
499
+ <LoginForm
500
+ {...defaultProps}
501
+ onSignIn={onSignIn}
502
+ onSuccess={onSuccess}
503
+ />
504
+ );
505
+
506
+ const emailInput = screen.getByLabelText('Email');
507
+ const passwordInput = screen.getByLabelText('Password');
508
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
509
+
510
+ await user.type(emailInput, 'test@example.com');
511
+ await user.type(passwordInput, 'password123');
512
+ await user.click(submitButton);
513
+
514
+ await waitFor(() => {
515
+ expect(onSuccess).toHaveBeenCalledTimes(1);
516
+ });
517
+ });
518
+
519
+ it('does not call onSuccess when not provided', async () => {
520
+ const user = userEvent.setup();
521
+ const onSignIn = vi.fn().mockResolvedValue(undefined);
522
+
523
+ renderWithProviders(
524
+ <LoginForm
525
+ {...defaultProps}
526
+ onSignIn={onSignIn}
527
+ />
528
+ );
529
+
530
+ const emailInput = screen.getByLabelText('Email');
531
+ const passwordInput = screen.getByLabelText('Password');
532
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
533
+
534
+ await user.type(emailInput, 'test@example.com');
535
+ await user.type(passwordInput, 'password123');
536
+ await user.click(submitButton);
537
+
538
+ // Should not throw error when onSuccess is not provided
539
+ await waitFor(() => {
540
+ expect(onSignIn).toHaveBeenCalledTimes(1);
541
+ });
542
+ });
543
+ });
544
+
545
+ describe('Accessibility', () => {
546
+ it('has proper form structure', () => {
547
+ renderWithProviders(<LoginForm {...defaultProps} />);
548
+
549
+ const form = screen.getByTestId('login-form');
550
+ expect(form).toBeInTheDocument();
551
+ expect(form).toHaveAttribute('data-testid', 'login-form');
552
+ });
553
+
554
+ it('has proper label associations', () => {
555
+ renderWithProviders(<LoginForm {...defaultProps} />);
556
+
557
+ const emailInput = screen.getByLabelText('Email');
558
+ const passwordInput = screen.getByLabelText('Password');
559
+
560
+ expect(emailInput).toHaveAttribute('id', 'email');
561
+ expect(passwordInput).toHaveAttribute('id', 'password');
562
+ });
563
+
564
+ it('has proper heading structure', () => {
565
+ renderWithProviders(<LoginForm {...defaultProps} />);
566
+
567
+ const title = screen.getByRole('heading', { level: 2 });
568
+ expect(title).toBeInTheDocument();
569
+ expect(title).toHaveTextContent('Sign In');
570
+ });
571
+
572
+ it('has accessible error messages', async () => {
573
+ const user = userEvent.setup();
574
+ const onSignIn = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
575
+
576
+ renderWithProviders(
577
+ <LoginForm
578
+ {...defaultProps}
579
+ onSignIn={onSignIn}
580
+ />
581
+ );
582
+
583
+ const emailInput = screen.getByLabelText('Email');
584
+ const passwordInput = screen.getByLabelText('Password');
585
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
586
+
587
+ await user.type(emailInput, 'test@example.com');
588
+ await user.type(passwordInput, 'password123');
589
+ await user.click(submitButton);
590
+
591
+ await waitFor(() => {
592
+ const alert = screen.getByTestId('alert');
593
+ expect(alert).toHaveAttribute('data-variant', 'destructive');
594
+ expect(alert).toBeInTheDocument();
595
+ });
596
+ });
597
+
598
+ it('supports keyboard navigation', async () => {
599
+ const user = userEvent.setup();
600
+ renderWithProviders(<LoginForm {...defaultProps} />);
601
+
602
+ const emailInput = screen.getByLabelText('Email');
603
+ const passwordInput = screen.getByLabelText('Password');
604
+
605
+ // Focus first input
606
+ emailInput.focus();
607
+ expect(document.activeElement).toBe(emailInput);
608
+
609
+ // Test that inputs are focusable
610
+ expect(emailInput).not.toHaveAttribute('tabindex', '-1');
611
+ expect(passwordInput).not.toHaveAttribute('tabindex', '-1');
612
+
613
+ // Test that inputs can be focused programmatically
614
+ passwordInput.focus();
615
+ expect(document.activeElement).toBe(passwordInput);
616
+
617
+ // Fill form to enable submit button
618
+ await user.type(emailInput, 'test@example.com');
619
+ await user.type(passwordInput, 'password123');
620
+
621
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
622
+ expect(submitButton).not.toHaveAttribute('tabindex', '-1');
623
+ expect(submitButton).not.toBeDisabled();
624
+
625
+ submitButton.focus();
626
+ expect(document.activeElement).toBe(submitButton);
627
+ });
628
+ });
629
+
630
+ describe('Edge Cases', () => {
631
+ it('handles empty string values', async () => {
632
+ const user = userEvent.setup();
633
+ renderWithProviders(<LoginForm {...defaultProps} />);
634
+
635
+ const emailInput = screen.getByLabelText('Email');
636
+ const passwordInput = screen.getByLabelText('Password');
637
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
638
+
639
+ await user.type(emailInput, ' ');
640
+ await user.type(passwordInput, ' ');
641
+ await user.clear(emailInput);
642
+ await user.clear(passwordInput);
643
+
644
+ expect(submitButton).toBeDisabled();
645
+ });
646
+
647
+ it('handles very long input values', async () => {
648
+ const user = userEvent.setup();
649
+ const longEmail = 'a'.repeat(100) + '@example.com';
650
+ const longPassword = 'p'.repeat(100);
651
+
652
+ renderWithProviders(<LoginForm {...defaultProps} />);
653
+
654
+ const emailInput = screen.getByLabelText('Email');
655
+ const passwordInput = screen.getByLabelText('Password');
656
+
657
+ await user.type(emailInput, longEmail);
658
+ await user.type(passwordInput, longPassword);
659
+
660
+ expect(emailInput).toHaveValue(longEmail);
661
+ expect(passwordInput).toHaveValue(longPassword);
662
+ });
663
+
664
+ it('handles special characters in inputs', async () => {
665
+ const user = userEvent.setup();
666
+ const specialEmail = 'test+tag@example.co.uk';
667
+ const specialPassword = 'P@ssw0rd!@#$%^&*()';
668
+
669
+ renderWithProviders(<LoginForm {...defaultProps} />);
670
+
671
+ const emailInput = screen.getByLabelText('Email');
672
+ const passwordInput = screen.getByLabelText('Password');
673
+
674
+ await user.type(emailInput, specialEmail);
675
+ await user.type(passwordInput, specialPassword);
676
+
677
+ expect(emailInput).toHaveValue(specialEmail);
678
+ expect(passwordInput).toHaveValue(specialPassword);
679
+ });
680
+
681
+ it('handles rapid form submissions', async () => {
682
+ const user = userEvent.setup();
683
+ const onSignIn = vi.fn().mockImplementation(() =>
684
+ new Promise(resolve => setTimeout(resolve, 100))
685
+ );
686
+
687
+ renderWithProviders(
688
+ <LoginForm
689
+ {...defaultProps}
690
+ onSignIn={onSignIn}
691
+ />
692
+ );
693
+
694
+ const emailInput = screen.getByLabelText('Email');
695
+ const passwordInput = screen.getByLabelText('Password');
696
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
697
+
698
+ await user.type(emailInput, 'test@example.com');
699
+ await user.type(passwordInput, 'password123');
700
+
701
+ // Rapid clicks - the component doesn't prevent multiple submissions
702
+ // so we expect multiple calls
703
+ await user.click(submitButton);
704
+ await user.click(submitButton);
705
+ await user.click(submitButton);
706
+
707
+ // The component allows multiple submissions, so we expect multiple calls
708
+ expect(onSignIn).toHaveBeenCalledTimes(3);
709
+ });
710
+ });
711
+
712
+ describe('Performance', () => {
713
+ it('does not re-render unnecessarily with stable props', () => {
714
+ const stableProps = {
715
+ onSignIn: vi.fn(),
716
+ onSuccess: vi.fn(),
717
+ onError: vi.fn(),
718
+ appName: 'Test App',
719
+ title: 'Test Title',
720
+ subtitle: 'Test Subtitle',
721
+ };
722
+
723
+ const { rerender } = renderWithProviders(
724
+ <LoginForm {...stableProps} />
725
+ );
726
+
727
+ const initialTitle = screen.getByText('Test Title');
728
+
729
+ // Re-render with same props
730
+ rerender(<LoginForm {...stableProps} />);
731
+
732
+ // Should still show the same title
733
+ expect(screen.getByText('Test Title')).toBeInTheDocument();
734
+ });
735
+
736
+ it('handles prop changes efficiently', () => {
737
+ const { rerender } = renderWithProviders(
738
+ <LoginForm {...defaultProps} appName="App 1" />
739
+ );
740
+
741
+ expect(screen.getByText('Sign in to App 1')).toBeInTheDocument();
742
+
743
+ rerender(<LoginForm {...defaultProps} appName="App 2" />);
744
+
745
+ expect(screen.getByText('Sign in to App 2')).toBeInTheDocument();
746
+ });
747
+ });
748
+
749
+ describe('Integration', () => {
750
+ it('works with form validation and submission flow', async () => {
751
+ const user = userEvent.setup();
752
+ const onSignIn = vi.fn().mockResolvedValue(undefined);
753
+ const onSuccess = vi.fn();
754
+
755
+ renderWithProviders(
756
+ <LoginForm
757
+ {...defaultProps}
758
+ onSignIn={onSignIn}
759
+ onSuccess={onSuccess}
760
+ />
761
+ );
762
+
763
+ const emailInput = screen.getByLabelText('Email');
764
+ const passwordInput = screen.getByLabelText('Password');
765
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
766
+
767
+ // Initially disabled
768
+ expect(submitButton).toBeDisabled();
769
+
770
+ // Fill form
771
+ await user.type(emailInput, 'test@example.com');
772
+ await user.type(passwordInput, 'password123');
773
+
774
+ // Now enabled
775
+ expect(submitButton).not.toBeDisabled();
776
+
777
+ // Submit
778
+ await user.click(submitButton);
779
+
780
+ await waitFor(() => {
781
+ expect(onSignIn).toHaveBeenCalledWith({
782
+ email: 'test@example.com',
783
+ password: 'password123'
784
+ });
785
+ expect(onSuccess).toHaveBeenCalledTimes(1);
786
+ });
787
+ });
788
+
789
+ it('handles complete error flow', async () => {
790
+ const user = userEvent.setup();
791
+ const onSignIn = vi.fn().mockRejectedValue(new Error('Network error'));
792
+ const onError = vi.fn();
793
+
794
+ renderWithProviders(
795
+ <LoginForm
796
+ {...defaultProps}
797
+ onSignIn={onSignIn}
798
+ onError={onError}
799
+ />
800
+ );
801
+
802
+ const emailInput = screen.getByLabelText('Email');
803
+ const passwordInput = screen.getByLabelText('Password');
804
+ const submitButton = screen.getByRole('button', { name: 'Sign In' });
805
+
806
+ await user.type(emailInput, 'test@example.com');
807
+ await user.type(passwordInput, 'password123');
808
+ await user.click(submitButton);
809
+
810
+ await waitFor(() => {
811
+ expect(screen.getByText('Network error')).toBeInTheDocument();
812
+ expect(onError).toHaveBeenCalledWith(expect.any(Error));
813
+ });
814
+ });
815
+ });
816
+ });