@jmruthers/pace-core 0.5.136 → 0.5.139
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-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
- package/dist/chunk-5JMOHWDI.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
- package/dist/chunk-BOOI7GK2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
- package/dist/chunk-TLT2ZR3L.js.map +1 -0
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +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 +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +84 -15
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/rbac/hooks/useCan.test.ts +24 -0
- package/src/rbac/hooks/usePermissions.ts +49 -12
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-L6PGMCMD.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /package/examples/{components → components 2}/index.ts +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file PasswordResetForm Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/PasswordReset
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for the PasswordResetForm component.
|
|
8
|
+
* Tests cover rendering, form submission, loading states, error handling,
|
|
9
|
+
* success states, and accessibility compliance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
14
|
+
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { PasswordResetForm, PasswordResetFormProps } from './PasswordResetForm';
|
|
17
|
+
|
|
18
|
+
// Mock the UnifiedAuthProvider
|
|
19
|
+
const mockResetPassword = vi.fn();
|
|
20
|
+
vi.mock('../../providers/UnifiedAuthProvider', () => ({
|
|
21
|
+
useUnifiedAuth: () => ({
|
|
22
|
+
resetPassword: mockResetPassword
|
|
23
|
+
})
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Mock child components
|
|
27
|
+
vi.mock('../Button/Button', () => ({
|
|
28
|
+
Button: ({ children, onClick, disabled, className, type, variant, ...props }: any) => (
|
|
29
|
+
<button
|
|
30
|
+
onClick={onClick}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
className={className}
|
|
33
|
+
type={type}
|
|
34
|
+
data-variant={variant}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</button>
|
|
39
|
+
)
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('../Input/Input', () => ({
|
|
43
|
+
Input: ({ value, onChange, placeholder, disabled, required, type, id, ...props }: any) => (
|
|
44
|
+
<input
|
|
45
|
+
id={id}
|
|
46
|
+
type={type}
|
|
47
|
+
value={value}
|
|
48
|
+
onChange={onChange}
|
|
49
|
+
placeholder={placeholder}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
required={required}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock('../Label', () => ({
|
|
58
|
+
Label: ({ children, htmlFor, ...props }: any) => (
|
|
59
|
+
<label htmlFor={htmlFor} {...props}>
|
|
60
|
+
{children}
|
|
61
|
+
</label>
|
|
62
|
+
)
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
describe('PasswordResetForm', () => {
|
|
66
|
+
const defaultProps: PasswordResetFormProps = {};
|
|
67
|
+
|
|
68
|
+
describe('Rendering', () => {
|
|
69
|
+
it('renders password reset form with all elements', () => {
|
|
70
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
71
|
+
|
|
72
|
+
expect(screen.getByRole('form')).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByText('Reset Password')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByText("Enter your email address and we'll send you a reset link.")).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByLabelText('Email Address')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders with custom className', () => {
|
|
81
|
+
const customClass = 'custom-password-reset-form';
|
|
82
|
+
render(<PasswordResetForm {...defaultProps} className={customClass} />);
|
|
83
|
+
|
|
84
|
+
const form = screen.getByRole('form');
|
|
85
|
+
expect(form).toHaveClass(customClass);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('has proper form structure and accessibility', () => {
|
|
89
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
90
|
+
|
|
91
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
92
|
+
expect(emailInput).toHaveAttribute('type', 'email');
|
|
93
|
+
expect(emailInput).toHaveAttribute('required');
|
|
94
|
+
expect(emailInput).toHaveAttribute('id', 'email');
|
|
95
|
+
|
|
96
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
97
|
+
expect(submitButton).toHaveAttribute('type', 'submit');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Form Interaction', () => {
|
|
102
|
+
it('updates email input value when user types', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
105
|
+
|
|
106
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
107
|
+
await user.type(emailInput, 'test@example.com');
|
|
108
|
+
|
|
109
|
+
expect(emailInput).toHaveValue('test@example.com');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('enables submit button when email is provided', async () => {
|
|
113
|
+
const user = userEvent.setup();
|
|
114
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
115
|
+
|
|
116
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
117
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
118
|
+
|
|
119
|
+
expect(submitButton).toBeDisabled();
|
|
120
|
+
|
|
121
|
+
await user.type(emailInput, 'test@example.com');
|
|
122
|
+
|
|
123
|
+
expect(submitButton).toBeEnabled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('disables submit button when email is empty', () => {
|
|
127
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
128
|
+
|
|
129
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
130
|
+
expect(submitButton).toBeDisabled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('disables submit button when email is only whitespace', async () => {
|
|
134
|
+
const user = userEvent.setup();
|
|
135
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
136
|
+
|
|
137
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
138
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
139
|
+
|
|
140
|
+
await user.type(emailInput, ' ');
|
|
141
|
+
|
|
142
|
+
expect(submitButton).toBeDisabled();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Form Submission', () => {
|
|
147
|
+
it('calls resetPassword with email when form is submitted', async () => {
|
|
148
|
+
const user = userEvent.setup();
|
|
149
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
150
|
+
|
|
151
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
152
|
+
|
|
153
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
154
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
155
|
+
|
|
156
|
+
await user.type(emailInput, 'test@example.com');
|
|
157
|
+
await user.click(submitButton);
|
|
158
|
+
|
|
159
|
+
expect(mockResetPassword).toHaveBeenCalledWith('test@example.com');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('prevents form submission when email is empty', async () => {
|
|
163
|
+
const user = userEvent.setup();
|
|
164
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
165
|
+
|
|
166
|
+
const form = screen.getByRole('form');
|
|
167
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
168
|
+
|
|
169
|
+
expect(submitButton).toBeDisabled();
|
|
170
|
+
await user.click(submitButton);
|
|
171
|
+
|
|
172
|
+
expect(mockResetPassword).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('prevents form submission when email is only whitespace', async () => {
|
|
176
|
+
const user = userEvent.setup();
|
|
177
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
178
|
+
|
|
179
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
180
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
181
|
+
|
|
182
|
+
await user.type(emailInput, ' ');
|
|
183
|
+
await user.click(submitButton);
|
|
184
|
+
|
|
185
|
+
expect(mockResetPassword).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Loading States', () => {
|
|
190
|
+
it('shows loading state during form submission', async () => {
|
|
191
|
+
const user = userEvent.setup();
|
|
192
|
+
mockResetPassword.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
193
|
+
|
|
194
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
195
|
+
|
|
196
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
197
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
198
|
+
|
|
199
|
+
await user.type(emailInput, 'test@example.com');
|
|
200
|
+
await user.click(submitButton);
|
|
201
|
+
|
|
202
|
+
expect(screen.getByRole('button', { name: 'Sending...' })).toBeInTheDocument();
|
|
203
|
+
expect(screen.getByRole('button', { name: 'Sending...' })).toBeDisabled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('disables email input during loading', async () => {
|
|
207
|
+
const user = userEvent.setup();
|
|
208
|
+
mockResetPassword.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
209
|
+
|
|
210
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
211
|
+
|
|
212
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
213
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
214
|
+
|
|
215
|
+
await user.type(emailInput, 'test@example.com');
|
|
216
|
+
await user.click(submitButton);
|
|
217
|
+
|
|
218
|
+
expect(emailInput).toBeDisabled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('resets loading state after successful submission', async () => {
|
|
222
|
+
const user = userEvent.setup();
|
|
223
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
224
|
+
|
|
225
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
226
|
+
|
|
227
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
228
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
229
|
+
|
|
230
|
+
await user.type(emailInput, 'test@example.com');
|
|
231
|
+
await user.click(submitButton);
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('resets loading state after failed submission', async () => {
|
|
239
|
+
const user = userEvent.setup();
|
|
240
|
+
mockResetPassword.mockResolvedValue({ error: { message: 'Network error' } });
|
|
241
|
+
|
|
242
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
243
|
+
|
|
244
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
245
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
246
|
+
|
|
247
|
+
await user.type(emailInput, 'test@example.com');
|
|
248
|
+
await user.click(submitButton);
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
|
|
252
|
+
expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeEnabled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('Success State', () => {
|
|
258
|
+
it('shows success message after successful password reset', async () => {
|
|
259
|
+
const user = userEvent.setup();
|
|
260
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
261
|
+
|
|
262
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
263
|
+
|
|
264
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
265
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
266
|
+
|
|
267
|
+
await user.type(emailInput, 'test@example.com');
|
|
268
|
+
await user.click(submitButton);
|
|
269
|
+
|
|
270
|
+
await waitFor(() => {
|
|
271
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
272
|
+
expect(screen.getByText('We have sent a password reset link to test@example.com')).toBeInTheDocument();
|
|
273
|
+
expect(screen.getByRole('button', { name: 'Send another email' })).toBeInTheDocument();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('calls onSuccess callback when reset is successful', async () => {
|
|
278
|
+
const user = userEvent.setup();
|
|
279
|
+
const onSuccess = vi.fn();
|
|
280
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
281
|
+
|
|
282
|
+
render(<PasswordResetForm {...defaultProps} onSuccess={onSuccess} />);
|
|
283
|
+
|
|
284
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
285
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
286
|
+
|
|
287
|
+
await user.type(emailInput, 'test@example.com');
|
|
288
|
+
await user.click(submitButton);
|
|
289
|
+
|
|
290
|
+
await waitFor(() => {
|
|
291
|
+
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('allows sending another email from success state', async () => {
|
|
296
|
+
const user = userEvent.setup();
|
|
297
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
298
|
+
|
|
299
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
300
|
+
|
|
301
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
302
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
303
|
+
|
|
304
|
+
await user.type(emailInput, 'test@example.com');
|
|
305
|
+
await user.click(submitButton);
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const sendAnotherButton = screen.getByRole('button', { name: 'Send another email' });
|
|
312
|
+
await user.click(sendAnotherButton);
|
|
313
|
+
|
|
314
|
+
expect(screen.getByText('Reset Password')).toBeInTheDocument();
|
|
315
|
+
expect(screen.getByLabelText('Email Address')).toBeInTheDocument();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('resets form state when sending another email', async () => {
|
|
319
|
+
const user = userEvent.setup();
|
|
320
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
321
|
+
|
|
322
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
323
|
+
|
|
324
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
325
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
326
|
+
|
|
327
|
+
await user.type(emailInput, 'test@example.com');
|
|
328
|
+
await user.click(submitButton);
|
|
329
|
+
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const sendAnotherButton = screen.getByRole('button', { name: 'Send another email' });
|
|
335
|
+
await user.click(sendAnotherButton);
|
|
336
|
+
|
|
337
|
+
// The form should be reset and show the original form
|
|
338
|
+
expect(screen.getByText('Reset Password')).toBeInTheDocument();
|
|
339
|
+
expect(screen.getByLabelText('Email Address')).toBeInTheDocument();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('Error Handling', () => {
|
|
344
|
+
it('displays error message when resetPassword returns error', async () => {
|
|
345
|
+
const user = userEvent.setup();
|
|
346
|
+
const errorMessage = 'Email not found';
|
|
347
|
+
mockResetPassword.mockResolvedValue({ error: { message: errorMessage } });
|
|
348
|
+
|
|
349
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
350
|
+
|
|
351
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
352
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
353
|
+
|
|
354
|
+
await user.type(emailInput, 'test@example.com');
|
|
355
|
+
await user.click(submitButton);
|
|
356
|
+
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
359
|
+
expect(screen.getByText(errorMessage)).toHaveAttribute('role', 'alert');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('calls onError callback when resetPassword returns error', async () => {
|
|
364
|
+
const user = userEvent.setup();
|
|
365
|
+
const onError = vi.fn();
|
|
366
|
+
const errorMessage = 'Email not found';
|
|
367
|
+
mockResetPassword.mockResolvedValue({ error: { message: errorMessage } });
|
|
368
|
+
|
|
369
|
+
render(<PasswordResetForm {...defaultProps} onError={onError} />);
|
|
370
|
+
|
|
371
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
372
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
373
|
+
|
|
374
|
+
await user.type(emailInput, 'test@example.com');
|
|
375
|
+
await user.click(submitButton);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({
|
|
379
|
+
message: errorMessage
|
|
380
|
+
}));
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('displays error message when resetPassword throws exception', async () => {
|
|
385
|
+
const user = userEvent.setup();
|
|
386
|
+
const errorMessage = 'Network error';
|
|
387
|
+
mockResetPassword.mockRejectedValue(new Error(errorMessage));
|
|
388
|
+
|
|
389
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
390
|
+
|
|
391
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
392
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
393
|
+
|
|
394
|
+
await user.type(emailInput, 'test@example.com');
|
|
395
|
+
await user.click(submitButton);
|
|
396
|
+
|
|
397
|
+
await waitFor(() => {
|
|
398
|
+
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
399
|
+
expect(screen.getByText(errorMessage)).toHaveAttribute('role', 'alert');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('calls onError callback when resetPassword throws exception', async () => {
|
|
404
|
+
const user = userEvent.setup();
|
|
405
|
+
const onError = vi.fn();
|
|
406
|
+
const errorMessage = 'Network error';
|
|
407
|
+
mockResetPassword.mockRejectedValue(new Error(errorMessage));
|
|
408
|
+
|
|
409
|
+
render(<PasswordResetForm {...defaultProps} onError={onError} />);
|
|
410
|
+
|
|
411
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
412
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
413
|
+
|
|
414
|
+
await user.type(emailInput, 'test@example.com');
|
|
415
|
+
await user.click(submitButton);
|
|
416
|
+
|
|
417
|
+
await waitFor(() => {
|
|
418
|
+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({
|
|
419
|
+
message: errorMessage
|
|
420
|
+
}));
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('handles non-Error exceptions gracefully', async () => {
|
|
425
|
+
const user = userEvent.setup();
|
|
426
|
+
mockResetPassword.mockRejectedValue('String error');
|
|
427
|
+
|
|
428
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
429
|
+
|
|
430
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
431
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
432
|
+
|
|
433
|
+
await user.type(emailInput, 'test@example.com');
|
|
434
|
+
await user.click(submitButton);
|
|
435
|
+
|
|
436
|
+
await waitFor(() => {
|
|
437
|
+
expect(screen.getByText('An unexpected error occurred')).toBeInTheDocument();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('clears error message when form is resubmitted', async () => {
|
|
442
|
+
const user = userEvent.setup();
|
|
443
|
+
mockResetPassword
|
|
444
|
+
.mockResolvedValueOnce({ error: { message: 'First error' } })
|
|
445
|
+
.mockResolvedValueOnce({ error: null });
|
|
446
|
+
|
|
447
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
448
|
+
|
|
449
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
450
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
451
|
+
|
|
452
|
+
await user.type(emailInput, 'test@example.com');
|
|
453
|
+
await user.click(submitButton);
|
|
454
|
+
|
|
455
|
+
await waitFor(() => {
|
|
456
|
+
expect(screen.getByText('First error')).toBeInTheDocument();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await user.click(submitButton);
|
|
460
|
+
|
|
461
|
+
await waitFor(() => {
|
|
462
|
+
expect(screen.queryByText('First error')).not.toBeInTheDocument();
|
|
463
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('Accessibility', () => {
|
|
469
|
+
it('has proper form labels and associations', () => {
|
|
470
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
471
|
+
|
|
472
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
473
|
+
expect(emailInput).toHaveAttribute('id', 'email');
|
|
474
|
+
expect(emailInput).toHaveAttribute('type', 'email');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('announces errors to screen readers', async () => {
|
|
478
|
+
const user = userEvent.setup();
|
|
479
|
+
mockResetPassword.mockResolvedValue({ error: { message: 'Test error' } });
|
|
480
|
+
|
|
481
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
482
|
+
|
|
483
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
484
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
485
|
+
|
|
486
|
+
await user.type(emailInput, 'test@example.com');
|
|
487
|
+
await user.click(submitButton);
|
|
488
|
+
|
|
489
|
+
await waitFor(() => {
|
|
490
|
+
const errorElement = screen.getByText('Test error');
|
|
491
|
+
expect(errorElement).toHaveAttribute('role', 'alert');
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('maintains focus management during state changes', async () => {
|
|
496
|
+
const user = userEvent.setup();
|
|
497
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
498
|
+
|
|
499
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
500
|
+
|
|
501
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
502
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
503
|
+
|
|
504
|
+
await user.type(emailInput, 'test@example.com');
|
|
505
|
+
await user.click(submitButton);
|
|
506
|
+
|
|
507
|
+
await waitFor(() => {
|
|
508
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const sendAnotherButton = screen.getByRole('button', { name: 'Send another email' });
|
|
512
|
+
expect(sendAnotherButton).toBeInTheDocument();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe('Edge Cases', () => {
|
|
517
|
+
it('handles empty onSuccess callback gracefully', async () => {
|
|
518
|
+
const user = userEvent.setup();
|
|
519
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
520
|
+
|
|
521
|
+
render(<PasswordResetForm {...defaultProps} onSuccess={undefined} />);
|
|
522
|
+
|
|
523
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
524
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
525
|
+
|
|
526
|
+
await user.type(emailInput, 'test@example.com');
|
|
527
|
+
await user.click(submitButton);
|
|
528
|
+
|
|
529
|
+
await waitFor(() => {
|
|
530
|
+
expect(screen.getByText('Check your email')).toBeInTheDocument();
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('handles empty onError callback gracefully', async () => {
|
|
535
|
+
const user = userEvent.setup();
|
|
536
|
+
mockResetPassword.mockResolvedValue({ error: { message: 'Test error' } });
|
|
537
|
+
|
|
538
|
+
render(<PasswordResetForm {...defaultProps} onError={undefined} />);
|
|
539
|
+
|
|
540
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
541
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
542
|
+
|
|
543
|
+
await user.type(emailInput, 'test@example.com');
|
|
544
|
+
await user.click(submitButton);
|
|
545
|
+
|
|
546
|
+
await waitFor(() => {
|
|
547
|
+
expect(screen.getByText('Test error')).toBeInTheDocument();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('handles very long email addresses', async () => {
|
|
552
|
+
const user = userEvent.setup();
|
|
553
|
+
const longEmail = 'a'.repeat(100) + '@example.com';
|
|
554
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
555
|
+
|
|
556
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
557
|
+
|
|
558
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
559
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
560
|
+
|
|
561
|
+
await user.type(emailInput, longEmail);
|
|
562
|
+
await user.click(submitButton);
|
|
563
|
+
|
|
564
|
+
expect(mockResetPassword).toHaveBeenCalledWith(longEmail);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('handles special characters in email', async () => {
|
|
568
|
+
const user = userEvent.setup();
|
|
569
|
+
const specialEmail = 'test+tag@example.co.uk';
|
|
570
|
+
mockResetPassword.mockResolvedValue({ error: null });
|
|
571
|
+
|
|
572
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
573
|
+
|
|
574
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
575
|
+
const submitButton = screen.getByRole('button', { name: 'Send Reset Link' });
|
|
576
|
+
|
|
577
|
+
await user.type(emailInput, specialEmail);
|
|
578
|
+
await user.click(submitButton);
|
|
579
|
+
|
|
580
|
+
expect(mockResetPassword).toHaveBeenCalledWith(specialEmail);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe('Performance', () => {
|
|
585
|
+
it('handles rapid input changes efficiently', async () => {
|
|
586
|
+
const user = userEvent.setup();
|
|
587
|
+
render(<PasswordResetForm {...defaultProps} />);
|
|
588
|
+
|
|
589
|
+
const emailInput = screen.getByLabelText('Email Address');
|
|
590
|
+
|
|
591
|
+
// Type rapidly to test performance
|
|
592
|
+
await user.type(emailInput, 'test@example.com');
|
|
593
|
+
|
|
594
|
+
expect(emailInput).toHaveValue('test@example.com');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
});
|