@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,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Input Component Tests
|
|
3
|
+
* @description Comprehensive tests for Input and InputGroup components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { screen } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
|
+
import { Input, InputGroup } from './Input';
|
|
11
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
12
|
+
|
|
13
|
+
describe('Input Component', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Basic rendering tests
|
|
19
|
+
describe('Rendering', () => {
|
|
20
|
+
it('renders with default props', () => {
|
|
21
|
+
renderWithProviders(<Input />);
|
|
22
|
+
const input = screen.getByRole('textbox');
|
|
23
|
+
expect(input).toBeInTheDocument();
|
|
24
|
+
expect(input).toHaveProperty('type', 'text');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders with custom placeholder', () => {
|
|
28
|
+
renderWithProviders(<Input placeholder="Enter your name" />);
|
|
29
|
+
expect(screen.getByPlaceholderText('Enter your name')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders with custom type', () => {
|
|
33
|
+
renderWithProviders(<Input type="email" />);
|
|
34
|
+
const input = screen.getByRole('textbox');
|
|
35
|
+
expect(input).toHaveAttribute('type', 'email');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders with custom className', () => {
|
|
39
|
+
renderWithProviders(<Input className="custom-class" />);
|
|
40
|
+
const input = screen.getByRole('textbox');
|
|
41
|
+
expect(input).toHaveClass('custom-class');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders with value', () => {
|
|
45
|
+
renderWithProviders(<Input value="test value" />);
|
|
46
|
+
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders with defaultValue', () => {
|
|
50
|
+
renderWithProviders(<Input defaultValue="default value" />);
|
|
51
|
+
expect(screen.getByDisplayValue('default value')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Variant tests
|
|
56
|
+
describe('Variants', () => {
|
|
57
|
+
it('renders with default variant', () => {
|
|
58
|
+
renderWithProviders(<Input variant="default" />);
|
|
59
|
+
const input = screen.getByRole('textbox');
|
|
60
|
+
expect(input).toHaveClass('border-input');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders with destructive variant', () => {
|
|
64
|
+
renderWithProviders(<Input variant="destructive" />);
|
|
65
|
+
const input = screen.getByRole('textbox');
|
|
66
|
+
expect(input).toHaveClass('border-destructive', 'focus-visible:ring-destructive');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('applies destructive styling when error is true', () => {
|
|
70
|
+
renderWithProviders(<Input error={true} />);
|
|
71
|
+
const input = screen.getByRole('textbox');
|
|
72
|
+
expect(input).toHaveClass('border-destructive', 'focus-visible:ring-destructive');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Size tests
|
|
77
|
+
describe('Sizes', () => {
|
|
78
|
+
it('renders with small size', () => {
|
|
79
|
+
renderWithProviders(<Input size="sm" />);
|
|
80
|
+
const input = screen.getByRole('textbox');
|
|
81
|
+
expect(input).toHaveClass('h-8', 'px-2', 'py-1', 'text-xs');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders with medium size (default)', () => {
|
|
85
|
+
renderWithProviders(<Input size="md" />);
|
|
86
|
+
const input = screen.getByRole('textbox');
|
|
87
|
+
expect(input).toHaveClass('h-9', 'px-3', 'py-2', 'text-sm');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders with large size', () => {
|
|
91
|
+
renderWithProviders(<Input size="lg" />);
|
|
92
|
+
const input = screen.getByRole('textbox');
|
|
93
|
+
expect(input).toHaveClass('h-10', 'px-4', 'py-3', 'text-base');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Event handling tests
|
|
98
|
+
describe('Event Handling', () => {
|
|
99
|
+
it('handles onChange events', async () => {
|
|
100
|
+
const handleChange = vi.fn();
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
|
|
103
|
+
renderWithProviders(<Input onChange={handleChange} />);
|
|
104
|
+
|
|
105
|
+
const input = screen.getByRole('textbox');
|
|
106
|
+
await user.type(input, 'test input');
|
|
107
|
+
|
|
108
|
+
expect(handleChange).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles onFocus events', async () => {
|
|
112
|
+
const handleFocus = vi.fn();
|
|
113
|
+
const user = userEvent.setup();
|
|
114
|
+
|
|
115
|
+
renderWithProviders(<Input onFocus={handleFocus} />);
|
|
116
|
+
|
|
117
|
+
const input = screen.getByRole('textbox');
|
|
118
|
+
await user.click(input);
|
|
119
|
+
|
|
120
|
+
expect(handleFocus).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('handles onBlur events', async () => {
|
|
124
|
+
const handleBlur = vi.fn();
|
|
125
|
+
const user = userEvent.setup();
|
|
126
|
+
|
|
127
|
+
renderWithProviders(<Input onBlur={handleBlur} />);
|
|
128
|
+
|
|
129
|
+
const input = screen.getByRole('textbox');
|
|
130
|
+
await user.click(input);
|
|
131
|
+
await user.tab();
|
|
132
|
+
|
|
133
|
+
expect(handleBlur).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles onKeyDown events', async () => {
|
|
137
|
+
const handleKeyDown = vi.fn();
|
|
138
|
+
const user = userEvent.setup();
|
|
139
|
+
|
|
140
|
+
renderWithProviders(<Input onKeyDown={handleKeyDown} />);
|
|
141
|
+
|
|
142
|
+
const input = screen.getByRole('textbox');
|
|
143
|
+
await user.type(input, 'a');
|
|
144
|
+
|
|
145
|
+
expect(handleKeyDown).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// State management tests
|
|
150
|
+
describe('State Management', () => {
|
|
151
|
+
it('handles controlled state', () => {
|
|
152
|
+
const { rerender } = renderWithProviders(
|
|
153
|
+
<Input value="initial" />
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(screen.getByDisplayValue('initial')).toBeInTheDocument();
|
|
157
|
+
|
|
158
|
+
rerender(<Input value="updated" />);
|
|
159
|
+
expect(screen.getByDisplayValue('updated')).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('handles uncontrolled state', () => {
|
|
163
|
+
renderWithProviders(<Input defaultValue="default" />);
|
|
164
|
+
expect(screen.getByDisplayValue('default')).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles disabled state', () => {
|
|
168
|
+
renderWithProviders(<Input disabled />);
|
|
169
|
+
const input = screen.getByRole('textbox');
|
|
170
|
+
expect(input).toBeDisabled();
|
|
171
|
+
expect(input).toHaveClass('disabled:cursor-not-allowed', 'disabled:opacity-50');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Accessibility tests
|
|
176
|
+
describe('Accessibility', () => {
|
|
177
|
+
it('has proper ARIA attributes', () => {
|
|
178
|
+
renderWithProviders(<Input aria-label="Test input" />);
|
|
179
|
+
const input = screen.getByRole('textbox');
|
|
180
|
+
expect(input).toHaveAttribute('aria-label', 'Test input');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('supports aria-describedby', () => {
|
|
184
|
+
renderWithProviders(
|
|
185
|
+
<div>
|
|
186
|
+
<Input aria-describedby="help-text" />
|
|
187
|
+
<div id="help-text">This is help text</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
const input = screen.getByRole('textbox');
|
|
191
|
+
expect(input).toHaveAttribute('aria-describedby', 'help-text');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('supports aria-invalid for error states', () => {
|
|
195
|
+
renderWithProviders(<Input aria-invalid="true" />);
|
|
196
|
+
const input = screen.getByRole('textbox');
|
|
197
|
+
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('is keyboard accessible', () => {
|
|
201
|
+
renderWithProviders(<Input />);
|
|
202
|
+
const input = screen.getByRole('textbox');
|
|
203
|
+
expect(input).not.toHaveAttribute('tabindex', '-1');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('supports screen readers with proper focus management', async () => {
|
|
207
|
+
const user = userEvent.setup();
|
|
208
|
+
renderWithProviders(<Input />);
|
|
209
|
+
|
|
210
|
+
const input = screen.getByRole('textbox');
|
|
211
|
+
await user.click(input);
|
|
212
|
+
|
|
213
|
+
expect(input).toHaveFocus();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Error handling tests
|
|
218
|
+
describe('Error Handling', () => {
|
|
219
|
+
it('handles error state styling', () => {
|
|
220
|
+
renderWithProviders(<Input error={true} />);
|
|
221
|
+
const input = screen.getByRole('textbox');
|
|
222
|
+
expect(input).toHaveClass('border-destructive', 'focus-visible:ring-destructive');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles invalid input gracefully', () => {
|
|
226
|
+
renderWithProviders(<Input type="email" value="invalid-email" />);
|
|
227
|
+
const input = screen.getByRole('textbox');
|
|
228
|
+
expect(input).toHaveValue('invalid-email');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Integration tests
|
|
233
|
+
describe('Integration', () => {
|
|
234
|
+
it('works with forms', async () => {
|
|
235
|
+
const handleSubmit = vi.fn((e) => e.preventDefault());
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
|
|
238
|
+
renderWithProviders(
|
|
239
|
+
<form onSubmit={handleSubmit}>
|
|
240
|
+
<Input name="test-input" />
|
|
241
|
+
<button type="submit">Submit</button>
|
|
242
|
+
</form>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
await user.click(screen.getByRole('button', { name: 'Submit' }));
|
|
246
|
+
expect(handleSubmit).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('works with labels', () => {
|
|
250
|
+
renderWithProviders(
|
|
251
|
+
<div>
|
|
252
|
+
<label htmlFor="test-input">Test Label</label>
|
|
253
|
+
<Input id="test-input" />
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const input = screen.getByLabelText('Test Label');
|
|
258
|
+
expect(input).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('forwards ref correctly', () => {
|
|
262
|
+
const ref = React.createRef<HTMLInputElement>();
|
|
263
|
+
renderWithProviders(<Input ref={ref} />);
|
|
264
|
+
|
|
265
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('InputGroup Component', () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
vi.clearAllMocks();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Basic rendering tests
|
|
276
|
+
describe('Rendering', () => {
|
|
277
|
+
it('renders with default props', () => {
|
|
278
|
+
renderWithProviders(
|
|
279
|
+
<InputGroup>
|
|
280
|
+
<Input placeholder="Input 1" />
|
|
281
|
+
<Input placeholder="Input 2" />
|
|
282
|
+
</InputGroup>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(screen.getByPlaceholderText('Input 1')).toBeInTheDocument();
|
|
286
|
+
expect(screen.getByPlaceholderText('Input 2')).toBeInTheDocument();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('renders with custom className', () => {
|
|
290
|
+
renderWithProviders(
|
|
291
|
+
<InputGroup className="custom-group">
|
|
292
|
+
<Input placeholder="Input 1" />
|
|
293
|
+
</InputGroup>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
297
|
+
expect(group).toHaveClass('custom-group');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Orientation tests
|
|
302
|
+
describe('Orientation', () => {
|
|
303
|
+
it('renders with vertical orientation (default)', () => {
|
|
304
|
+
renderWithProviders(
|
|
305
|
+
<InputGroup orientation="vertical">
|
|
306
|
+
<Input placeholder="Input 1" />
|
|
307
|
+
<Input placeholder="Input 2" />
|
|
308
|
+
</InputGroup>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
312
|
+
expect(group).toHaveClass('flex-col');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('renders with horizontal orientation', () => {
|
|
316
|
+
renderWithProviders(
|
|
317
|
+
<InputGroup orientation="horizontal">
|
|
318
|
+
<Input placeholder="Input 1" />
|
|
319
|
+
<Input placeholder="Input 2" />
|
|
320
|
+
</InputGroup>
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
324
|
+
expect(group).toHaveClass('flex-row', 'items-end');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Spacing tests
|
|
329
|
+
describe('Spacing', () => {
|
|
330
|
+
it('applies small spacing for vertical orientation', () => {
|
|
331
|
+
renderWithProviders(
|
|
332
|
+
<InputGroup orientation="vertical" spacing="sm">
|
|
333
|
+
<Input placeholder="Input 1" />
|
|
334
|
+
<Input placeholder="Input 2" />
|
|
335
|
+
</InputGroup>
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
339
|
+
expect(group).toHaveClass('space-y-2');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('applies medium spacing for vertical orientation (default)', () => {
|
|
343
|
+
renderWithProviders(
|
|
344
|
+
<InputGroup orientation="vertical" spacing="md">
|
|
345
|
+
<Input placeholder="Input 1" />
|
|
346
|
+
<Input placeholder="Input 2" />
|
|
347
|
+
</InputGroup>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
351
|
+
expect(group).toHaveClass('space-y-4');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('applies large spacing for vertical orientation', () => {
|
|
355
|
+
renderWithProviders(
|
|
356
|
+
<InputGroup orientation="vertical" spacing="lg">
|
|
357
|
+
<Input placeholder="Input 1" />
|
|
358
|
+
<Input placeholder="Input 2" />
|
|
359
|
+
</InputGroup>
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
363
|
+
expect(group).toHaveClass('space-y-6');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('applies small spacing for horizontal orientation', () => {
|
|
367
|
+
renderWithProviders(
|
|
368
|
+
<InputGroup orientation="horizontal" spacing="sm">
|
|
369
|
+
<Input placeholder="Input 1" />
|
|
370
|
+
<Input placeholder="Input 2" />
|
|
371
|
+
</InputGroup>
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
375
|
+
expect(group).toHaveClass('space-x-2');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('applies medium spacing for horizontal orientation (default)', () => {
|
|
379
|
+
renderWithProviders(
|
|
380
|
+
<InputGroup orientation="horizontal" spacing="md">
|
|
381
|
+
<Input placeholder="Input 1" />
|
|
382
|
+
<Input placeholder="Input 2" />
|
|
383
|
+
</InputGroup>
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
387
|
+
expect(group).toHaveClass('space-x-4');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('applies large spacing for horizontal orientation', () => {
|
|
391
|
+
renderWithProviders(
|
|
392
|
+
<InputGroup orientation="horizontal" spacing="lg">
|
|
393
|
+
<Input placeholder="Input 1" />
|
|
394
|
+
<Input placeholder="Input 2" />
|
|
395
|
+
</InputGroup>
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const group = screen.getByPlaceholderText('Input 1').parentElement;
|
|
399
|
+
expect(group).toHaveClass('space-x-6');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Accessibility tests
|
|
404
|
+
describe('Accessibility', () => {
|
|
405
|
+
it('forwards ref correctly', () => {
|
|
406
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
407
|
+
renderWithProviders(
|
|
408
|
+
<InputGroup ref={ref}>
|
|
409
|
+
<Input placeholder="Input 1" />
|
|
410
|
+
</InputGroup>
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('maintains proper focus order', async () => {
|
|
417
|
+
const user = userEvent.setup();
|
|
418
|
+
renderWithProviders(
|
|
419
|
+
<InputGroup>
|
|
420
|
+
<Input placeholder="Input 1" />
|
|
421
|
+
<Input placeholder="Input 2" />
|
|
422
|
+
</InputGroup>
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const input1 = screen.getByPlaceholderText('Input 1');
|
|
426
|
+
const input2 = screen.getByPlaceholderText('Input 2');
|
|
427
|
+
|
|
428
|
+
await user.click(input1);
|
|
429
|
+
expect(input1).toHaveFocus();
|
|
430
|
+
|
|
431
|
+
await user.tab();
|
|
432
|
+
expect(input2).toHaveFocus();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Integration tests
|
|
437
|
+
describe('Integration', () => {
|
|
438
|
+
it('works with multiple inputs', () => {
|
|
439
|
+
renderWithProviders(
|
|
440
|
+
<InputGroup>
|
|
441
|
+
<Input placeholder="First Name" />
|
|
442
|
+
<Input placeholder="Last Name" />
|
|
443
|
+
<Input placeholder="Email" type="email" />
|
|
444
|
+
</InputGroup>
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
expect(screen.getByPlaceholderText('First Name')).toBeInTheDocument();
|
|
448
|
+
expect(screen.getByPlaceholderText('Last Name')).toBeInTheDocument();
|
|
449
|
+
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('works with different input types', () => {
|
|
453
|
+
renderWithProviders(
|
|
454
|
+
<InputGroup>
|
|
455
|
+
<Input placeholder="Text" type="text" />
|
|
456
|
+
<Input placeholder="Email" type="email" />
|
|
457
|
+
<Input placeholder="Password" type="password" />
|
|
458
|
+
</InputGroup>
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(screen.getByPlaceholderText('Text')).toHaveAttribute('type', 'text');
|
|
462
|
+
expect(screen.getByPlaceholderText('Email')).toHaveAttribute('type', 'email');
|
|
463
|
+
expect(screen.getByPlaceholderText('Password')).toHaveAttribute('type', 'password');
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|