@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.
- 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 -55
- 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 +952 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -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 +364 -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 +551 -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 -55
- 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,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
|
+
});
|