@jmruthers/pace-core 0.5.114 → 0.5.116
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/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
- package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- 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 +35 -12
- 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/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 +71 -0
- 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 +122 -0
- 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 +27 -27
- 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 +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- 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/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 +43 -16
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +32 -17
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ImportModal.tsx +25 -2
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.test.tsx +8 -7
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +11 -12
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-4OX5PXHX.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-5YIZFEUQ.js.map +0 -1
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Import Modal Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for ImportModal component following testing guidelines.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and user interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { ImportModal } from '../ImportModal';
|
|
16
|
+
|
|
17
|
+
// Mock Dialog components - Use simple mock without importActual to avoid Radix UI dependencies
|
|
18
|
+
vi.mock('../../Dialog', () => ({
|
|
19
|
+
Dialog: ({ children, open, onOpenChange }: any) => (
|
|
20
|
+
open ? <div role="dialog" data-testid="dialog">{children}</div> : null
|
|
21
|
+
),
|
|
22
|
+
DialogContent: ({ children, className }: any) => (
|
|
23
|
+
<div data-testid="dialog-content" className={className}>{children}</div>
|
|
24
|
+
),
|
|
25
|
+
DialogHeader: ({ children }: any) => (
|
|
26
|
+
<div data-testid="dialog-header">{children}</div>
|
|
27
|
+
),
|
|
28
|
+
DialogTitle: ({ children }: any) => (
|
|
29
|
+
<h2 data-testid="dialog-title" role="heading" aria-level={2}>{children}</h2>
|
|
30
|
+
),
|
|
31
|
+
DialogDescription: ({ children }: any) => (
|
|
32
|
+
<p data-testid="dialog-description">{children}</p>
|
|
33
|
+
),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock Button component
|
|
37
|
+
vi.mock('../../Button/Button', () => ({
|
|
38
|
+
Button: ({ children, onClick, variant, size, disabled, className }: any) => (
|
|
39
|
+
<button
|
|
40
|
+
onClick={onClick}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
data-variant={variant}
|
|
43
|
+
data-size={size}
|
|
44
|
+
className={className}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</button>
|
|
48
|
+
),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Mock Input component
|
|
52
|
+
vi.mock('../../Input/Input', () => ({
|
|
53
|
+
Input: React.forwardRef(({ type, accept, onChange, className, ...props }: any, ref: any) => (
|
|
54
|
+
<input
|
|
55
|
+
ref={ref}
|
|
56
|
+
type={type}
|
|
57
|
+
accept={accept}
|
|
58
|
+
onChange={onChange}
|
|
59
|
+
className={className}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Mock lucide-react icons
|
|
66
|
+
vi.mock('lucide-react', () => ({
|
|
67
|
+
Upload: ({ className }: { className?: string }) => (
|
|
68
|
+
<span data-testid="upload-icon" className={className}>Upload</span>
|
|
69
|
+
),
|
|
70
|
+
FileText: ({ className }: { className?: string }) => (
|
|
71
|
+
<span data-testid="file-text-icon" className={className}>File</span>
|
|
72
|
+
),
|
|
73
|
+
AlertCircle: ({ className }: { className?: string }) => (
|
|
74
|
+
<span data-testid="alert-circle-icon" className={className}>Alert</span>
|
|
75
|
+
),
|
|
76
|
+
X: ({ className }: { className?: string }) => (
|
|
77
|
+
<span data-testid="x-icon" className={className}>X</span>
|
|
78
|
+
),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Mock logger
|
|
82
|
+
vi.mock('../../../utils/logger', () => ({
|
|
83
|
+
createLogger: () => ({
|
|
84
|
+
debug: vi.fn(),
|
|
85
|
+
info: vi.fn(),
|
|
86
|
+
warn: vi.fn(),
|
|
87
|
+
error: vi.fn(),
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
describe('[component] ImportModal', () => {
|
|
92
|
+
const defaultProps = {
|
|
93
|
+
isOpen: true,
|
|
94
|
+
onClose: vi.fn(),
|
|
95
|
+
onImport: vi.fn(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const createCSVFile = (content: string, filename = 'test.csv'): File => {
|
|
99
|
+
const blob = new Blob([content], { type: 'text/csv' });
|
|
100
|
+
return new File([blob], filename, { type: 'text/csv' });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
// Mock File.text() method for jsdom compatibility
|
|
106
|
+
// File.text() reads the file content asynchronously using FileReader
|
|
107
|
+
if (!File.prototype.text) {
|
|
108
|
+
Object.defineProperty(File.prototype, 'text', {
|
|
109
|
+
writable: true,
|
|
110
|
+
configurable: true,
|
|
111
|
+
value: async function(this: File) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
// Use FileReader to read the file content
|
|
114
|
+
const reader = new FileReader();
|
|
115
|
+
reader.onload = (e) => {
|
|
116
|
+
resolve(e.target?.result as string);
|
|
117
|
+
};
|
|
118
|
+
reader.onerror = () => {
|
|
119
|
+
reject(new Error('Failed to read file'));
|
|
120
|
+
};
|
|
121
|
+
// Read the file as text
|
|
122
|
+
reader.readAsText(this);
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
vi.clearAllMocks();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Rendering', () => {
|
|
134
|
+
it('returns null when modal is closed', () => {
|
|
135
|
+
const { container } = render(
|
|
136
|
+
<ImportModal {...defaultProps} isOpen={false} />
|
|
137
|
+
);
|
|
138
|
+
expect(container.firstChild).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('renders modal when open', () => {
|
|
142
|
+
render(<ImportModal {...defaultProps} />);
|
|
143
|
+
|
|
144
|
+
// Dialog renders with role="dialog" from Radix UI
|
|
145
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
146
|
+
// Check for content instead of testids
|
|
147
|
+
expect(screen.getByText('Import Data')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders default title', () => {
|
|
151
|
+
render(<ImportModal {...defaultProps} />);
|
|
152
|
+
|
|
153
|
+
// DialogTitle renders as h2
|
|
154
|
+
expect(screen.getByRole('heading', { name: 'Import Data' })).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renders custom title from config', () => {
|
|
158
|
+
render(
|
|
159
|
+
<ImportModal
|
|
160
|
+
{...defaultProps}
|
|
161
|
+
config={{ title: 'Custom Import Title' }}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// DialogTitle renders as h2
|
|
166
|
+
expect(screen.getByRole('heading', { name: 'Custom Import Title' })).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('renders default description', () => {
|
|
170
|
+
render(<ImportModal {...defaultProps} />);
|
|
171
|
+
|
|
172
|
+
// DialogDescription renders as p
|
|
173
|
+
expect(screen.getByText('Upload a CSV file to import multiple records at once.')).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('renders custom description from config', () => {
|
|
177
|
+
render(
|
|
178
|
+
<ImportModal
|
|
179
|
+
{...defaultProps}
|
|
180
|
+
config={{ description: 'Custom description' }}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// DialogDescription renders as p
|
|
185
|
+
expect(screen.getByText('Custom description')).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('renders file upload area', () => {
|
|
189
|
+
render(<ImportModal {...defaultProps} />);
|
|
190
|
+
|
|
191
|
+
expect(screen.getByText(/choose a csv file/i)).toBeInTheDocument();
|
|
192
|
+
expect(screen.getByRole('button', { name: /select file/i })).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('renders cancel button', () => {
|
|
196
|
+
render(<ImportModal {...defaultProps} />);
|
|
197
|
+
|
|
198
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('renders import button', () => {
|
|
202
|
+
render(<ImportModal {...defaultProps} />);
|
|
203
|
+
|
|
204
|
+
expect(screen.getByRole('button', { name: /import/i })).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('File Selection', () => {
|
|
209
|
+
it('displays selected file name', async () => {
|
|
210
|
+
const user = userEvent.setup();
|
|
211
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
212
|
+
const file = createCSVFile(csvContent);
|
|
213
|
+
|
|
214
|
+
render(<ImportModal {...defaultProps} />);
|
|
215
|
+
|
|
216
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
217
|
+
await user.upload(fileInput, file);
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(screen.getByText(`Selected: ${file.name}`)).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('resets file when modal closes and reopens', async () => {
|
|
225
|
+
const user = userEvent.setup();
|
|
226
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
227
|
+
const file = createCSVFile(csvContent);
|
|
228
|
+
|
|
229
|
+
const { rerender } = render(<ImportModal {...defaultProps} />);
|
|
230
|
+
|
|
231
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
232
|
+
await user.upload(fileInput, file);
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(screen.getByText(`Selected: ${file.name}`)).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
rerender(<ImportModal {...defaultProps} isOpen={false} />);
|
|
239
|
+
rerender(<ImportModal {...defaultProps} isOpen={true} />);
|
|
240
|
+
|
|
241
|
+
// File should be reset
|
|
242
|
+
expect(screen.queryByText(`Selected: ${file.name}`)).not.toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('CSV Parsing', () => {
|
|
247
|
+
it('parses valid CSV file correctly', async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
250
|
+
const file = createCSVFile(csvContent);
|
|
251
|
+
|
|
252
|
+
render(<ImportModal {...defaultProps} />);
|
|
253
|
+
|
|
254
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
255
|
+
await user.upload(fileInput, file);
|
|
256
|
+
|
|
257
|
+
// Wait for preview table to appear
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
const table = screen.queryByRole('table');
|
|
260
|
+
expect(table).toBeInTheDocument();
|
|
261
|
+
}, { timeout: 5000 });
|
|
262
|
+
|
|
263
|
+
// Once preview table is visible, check for table headers
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(screen.getByText(/name/i)).toBeInTheDocument();
|
|
266
|
+
expect(screen.getByText(/email/i)).toBeInTheDocument();
|
|
267
|
+
}, { timeout: 2000 });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('shows preview table with parsed data', async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
273
|
+
const file = createCSVFile(csvContent);
|
|
274
|
+
|
|
275
|
+
render(<ImportModal {...defaultProps} />);
|
|
276
|
+
|
|
277
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
278
|
+
await user.upload(fileInput, file);
|
|
279
|
+
|
|
280
|
+
// Wait for preview table to appear
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
const table = screen.queryByRole('table');
|
|
283
|
+
expect(table).toBeInTheDocument();
|
|
284
|
+
}, { timeout: 5000 });
|
|
285
|
+
|
|
286
|
+
// Then check for data
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
289
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
290
|
+
}, { timeout: 2000 });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('displays total row count', async () => {
|
|
294
|
+
const user = userEvent.setup();
|
|
295
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com\nBob,bob@example.com';
|
|
296
|
+
const file = createCSVFile(csvContent);
|
|
297
|
+
|
|
298
|
+
render(<ImportModal {...defaultProps} />);
|
|
299
|
+
|
|
300
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
301
|
+
await user.upload(fileInput, file);
|
|
302
|
+
|
|
303
|
+
// Wait for preview to appear first
|
|
304
|
+
await waitFor(() => {
|
|
305
|
+
const table = screen.queryByRole('table');
|
|
306
|
+
expect(table).toBeInTheDocument();
|
|
307
|
+
}, { timeout: 5000 });
|
|
308
|
+
|
|
309
|
+
// Then check for total row count
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(screen.getByText(/total rows to import: 3/i)).toBeInTheDocument();
|
|
312
|
+
}, { timeout: 2000 });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('handles CSV with quoted values', async () => {
|
|
316
|
+
const user = userEvent.setup();
|
|
317
|
+
const csvContent = 'name,email\n"John Doe","john@example.com"\n"Jane Smith","jane@example.com"';
|
|
318
|
+
const file = createCSVFile(csvContent);
|
|
319
|
+
|
|
320
|
+
render(<ImportModal {...defaultProps} />);
|
|
321
|
+
|
|
322
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
323
|
+
await user.upload(fileInput, file);
|
|
324
|
+
|
|
325
|
+
// Wait for preview table first
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
const table = screen.queryByRole('table');
|
|
328
|
+
expect(table).toBeInTheDocument();
|
|
329
|
+
}, { timeout: 5000 });
|
|
330
|
+
|
|
331
|
+
// Then check for data
|
|
332
|
+
await waitFor(() => {
|
|
333
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
334
|
+
}, { timeout: 2000 });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('handles CSV with commas in quoted values', async () => {
|
|
338
|
+
const user = userEvent.setup();
|
|
339
|
+
const csvContent = 'name,description\nJohn,"Description, with comma"\nJane,"Another, description"';
|
|
340
|
+
const file = createCSVFile(csvContent);
|
|
341
|
+
|
|
342
|
+
render(<ImportModal {...defaultProps} />);
|
|
343
|
+
|
|
344
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
345
|
+
await user.upload(fileInput, file);
|
|
346
|
+
|
|
347
|
+
// Wait for preview table first
|
|
348
|
+
await waitFor(() => {
|
|
349
|
+
const table = screen.queryByRole('table');
|
|
350
|
+
expect(table).toBeInTheDocument();
|
|
351
|
+
}, { timeout: 5000 });
|
|
352
|
+
|
|
353
|
+
// Then check for data
|
|
354
|
+
await waitFor(() => {
|
|
355
|
+
expect(screen.getByText(/description, with comma/i)).toBeInTheDocument();
|
|
356
|
+
}, { timeout: 2000 });
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Error Handling', () => {
|
|
361
|
+
it('displays error for invalid CSV file', async () => {
|
|
362
|
+
const user = userEvent.setup();
|
|
363
|
+
// Create a file that will cause parsing to fail
|
|
364
|
+
// CSV with only header row (no data rows) will trigger the error
|
|
365
|
+
const invalidContent = 'name,email';
|
|
366
|
+
const file = createCSVFile(invalidContent);
|
|
367
|
+
|
|
368
|
+
render(<ImportModal {...defaultProps} />);
|
|
369
|
+
|
|
370
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
371
|
+
await user.upload(fileInput, file);
|
|
372
|
+
|
|
373
|
+
// Wait for error to appear - CSV with only header should throw error
|
|
374
|
+
await waitFor(() => {
|
|
375
|
+
// Error should be displayed with AlertCircle icon or error message
|
|
376
|
+
const errorIcon = screen.queryByTestId('alert-circle-icon');
|
|
377
|
+
const errorText = screen.queryByText(/must have at least|error|failed/i);
|
|
378
|
+
expect(errorIcon || errorText).toBeInTheDocument();
|
|
379
|
+
}, { timeout: 5000 });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('displays error for CSV with only header row', async () => {
|
|
383
|
+
const user = userEvent.setup();
|
|
384
|
+
const csvContent = 'name,email';
|
|
385
|
+
const file = createCSVFile(csvContent);
|
|
386
|
+
|
|
387
|
+
render(<ImportModal {...defaultProps} />);
|
|
388
|
+
|
|
389
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
390
|
+
await user.upload(fileInput, file);
|
|
391
|
+
|
|
392
|
+
await waitFor(() => {
|
|
393
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('clears error when new file is selected', async () => {
|
|
398
|
+
const user = userEvent.setup();
|
|
399
|
+
const invalidFile = createCSVFile('invalid');
|
|
400
|
+
const validFile = createCSVFile('name,email\nJohn,john@example.com');
|
|
401
|
+
|
|
402
|
+
render(<ImportModal {...defaultProps} />);
|
|
403
|
+
|
|
404
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
405
|
+
await user.upload(fileInput, invalidFile);
|
|
406
|
+
|
|
407
|
+
await waitFor(() => {
|
|
408
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
409
|
+
}, { timeout: 3000 });
|
|
410
|
+
|
|
411
|
+
// Clear the file input value first
|
|
412
|
+
fileInput.value = '';
|
|
413
|
+
await user.upload(fileInput, validFile);
|
|
414
|
+
|
|
415
|
+
await waitFor(() => {
|
|
416
|
+
expect(screen.queryByTestId('alert-circle-icon')).not.toBeInTheDocument();
|
|
417
|
+
}, { timeout: 3000 });
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('Import Action', () => {
|
|
422
|
+
it('calls onImport with parsed data when import button is clicked', async () => {
|
|
423
|
+
const user = userEvent.setup();
|
|
424
|
+
const onImport = vi.fn();
|
|
425
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
426
|
+
const file = createCSVFile(csvContent);
|
|
427
|
+
|
|
428
|
+
render(<ImportModal {...defaultProps} onImport={onImport} />);
|
|
429
|
+
|
|
430
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
431
|
+
await user.upload(fileInput, file);
|
|
432
|
+
|
|
433
|
+
// Wait for preview to appear
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
const table = screen.queryByRole('table');
|
|
436
|
+
expect(table).toBeInTheDocument();
|
|
437
|
+
}, { timeout: 5000 });
|
|
438
|
+
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
441
|
+
}, { timeout: 2000 });
|
|
442
|
+
|
|
443
|
+
const importButton = screen.getByRole('button', { name: /import/i });
|
|
444
|
+
await user.click(importButton);
|
|
445
|
+
|
|
446
|
+
await waitFor(() => {
|
|
447
|
+
expect(onImport).toHaveBeenCalledTimes(1);
|
|
448
|
+
expect(onImport).toHaveBeenCalledWith(
|
|
449
|
+
expect.arrayContaining([
|
|
450
|
+
expect.objectContaining({
|
|
451
|
+
name: 'John',
|
|
452
|
+
email: 'john@example.com',
|
|
453
|
+
}),
|
|
454
|
+
])
|
|
455
|
+
);
|
|
456
|
+
}, { timeout: 3000 });
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('disables import button when no file is selected', () => {
|
|
460
|
+
render(<ImportModal {...defaultProps} />);
|
|
461
|
+
|
|
462
|
+
const importButton = screen.getByRole('button', { name: /import/i });
|
|
463
|
+
expect(importButton).toBeDisabled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('disables import button while processing', async () => {
|
|
467
|
+
const user = userEvent.setup();
|
|
468
|
+
const onImport = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
469
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
470
|
+
const file = createCSVFile(csvContent);
|
|
471
|
+
|
|
472
|
+
render(<ImportModal {...defaultProps} onImport={onImport} />);
|
|
473
|
+
|
|
474
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
475
|
+
await user.upload(fileInput, file);
|
|
476
|
+
|
|
477
|
+
// Wait for preview to appear
|
|
478
|
+
await waitFor(() => {
|
|
479
|
+
const table = screen.queryByRole('table');
|
|
480
|
+
expect(table).toBeInTheDocument();
|
|
481
|
+
}, { timeout: 5000 });
|
|
482
|
+
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
485
|
+
}, { timeout: 2000 });
|
|
486
|
+
|
|
487
|
+
const importButton = screen.getByRole('button', { name: /import/i });
|
|
488
|
+
await user.click(importButton);
|
|
489
|
+
|
|
490
|
+
await waitFor(() => {
|
|
491
|
+
expect(importButton).toBeDisabled();
|
|
492
|
+
}, { timeout: 1000 });
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('shows processing text while importing', async () => {
|
|
496
|
+
const user = userEvent.setup();
|
|
497
|
+
const onImport = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)));
|
|
498
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
499
|
+
const file = createCSVFile(csvContent);
|
|
500
|
+
|
|
501
|
+
render(<ImportModal {...defaultProps} onImport={onImport} />);
|
|
502
|
+
|
|
503
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
504
|
+
await user.upload(fileInput, file);
|
|
505
|
+
|
|
506
|
+
// Wait for preview to appear
|
|
507
|
+
await waitFor(() => {
|
|
508
|
+
const table = screen.queryByRole('table');
|
|
509
|
+
expect(table).toBeInTheDocument();
|
|
510
|
+
}, { timeout: 5000 });
|
|
511
|
+
|
|
512
|
+
await waitFor(() => {
|
|
513
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
514
|
+
}, { timeout: 2000 });
|
|
515
|
+
|
|
516
|
+
const importButton = screen.getByRole('button', { name: /import/i });
|
|
517
|
+
await user.click(importButton);
|
|
518
|
+
|
|
519
|
+
// Button text changes to "Processing..." when isProcessing is true
|
|
520
|
+
await waitFor(() => {
|
|
521
|
+
const processingButton = screen.getByRole('button', { name: /processing/i });
|
|
522
|
+
expect(processingButton).toBeInTheDocument();
|
|
523
|
+
}, { timeout: 1000 });
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('calls onClose after successful import', async () => {
|
|
527
|
+
const user = userEvent.setup();
|
|
528
|
+
const onClose = vi.fn();
|
|
529
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
530
|
+
const file = createCSVFile(csvContent);
|
|
531
|
+
|
|
532
|
+
render(<ImportModal {...defaultProps} onClose={onClose} />);
|
|
533
|
+
|
|
534
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
535
|
+
await user.upload(fileInput, file);
|
|
536
|
+
|
|
537
|
+
// Wait for preview to appear
|
|
538
|
+
await waitFor(() => {
|
|
539
|
+
const table = screen.queryByRole('table');
|
|
540
|
+
expect(table).toBeInTheDocument();
|
|
541
|
+
}, { timeout: 5000 });
|
|
542
|
+
|
|
543
|
+
await waitFor(() => {
|
|
544
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
545
|
+
}, { timeout: 2000 });
|
|
546
|
+
|
|
547
|
+
const importButton = screen.getByRole('button', { name: /import/i });
|
|
548
|
+
await user.click(importButton);
|
|
549
|
+
|
|
550
|
+
await waitFor(() => {
|
|
551
|
+
expect(onClose).toHaveBeenCalled();
|
|
552
|
+
}, { timeout: 3000 });
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe('Close Action', () => {
|
|
557
|
+
it('calls onClose when cancel button is clicked', async () => {
|
|
558
|
+
const user = userEvent.setup();
|
|
559
|
+
const onClose = vi.fn();
|
|
560
|
+
|
|
561
|
+
render(<ImportModal {...defaultProps} onClose={onClose} />);
|
|
562
|
+
|
|
563
|
+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
564
|
+
await user.click(cancelButton);
|
|
565
|
+
|
|
566
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('resets state when modal closes', async () => {
|
|
570
|
+
const user = userEvent.setup();
|
|
571
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
572
|
+
const file = createCSVFile(csvContent);
|
|
573
|
+
|
|
574
|
+
const { rerender } = render(<ImportModal {...defaultProps} />);
|
|
575
|
+
|
|
576
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
577
|
+
await user.upload(fileInput, file);
|
|
578
|
+
|
|
579
|
+
// Wait for preview to appear
|
|
580
|
+
await waitFor(() => {
|
|
581
|
+
const table = screen.queryByRole('table');
|
|
582
|
+
expect(table).toBeInTheDocument();
|
|
583
|
+
}, { timeout: 5000 });
|
|
584
|
+
|
|
585
|
+
await waitFor(() => {
|
|
586
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
587
|
+
}, { timeout: 2000 });
|
|
588
|
+
|
|
589
|
+
rerender(<ImportModal {...defaultProps} isOpen={false} />);
|
|
590
|
+
rerender(<ImportModal {...defaultProps} isOpen={true} />);
|
|
591
|
+
|
|
592
|
+
// State should be reset - wait for modal to reopen
|
|
593
|
+
await waitFor(() => {
|
|
594
|
+
expect(screen.queryByText('John')).not.toBeInTheDocument();
|
|
595
|
+
}, { timeout: 1000 });
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe('Custom Configuration', () => {
|
|
600
|
+
it('uses custom button texts from config', () => {
|
|
601
|
+
render(
|
|
602
|
+
<ImportModal
|
|
603
|
+
{...defaultProps}
|
|
604
|
+
config={{
|
|
605
|
+
selectFileButtonText: 'Browse Files',
|
|
606
|
+
importButtonText: 'Import Data',
|
|
607
|
+
cancelButtonText: 'Close',
|
|
608
|
+
}}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
expect(screen.getByRole('button', { name: /browse files/i })).toBeInTheDocument();
|
|
613
|
+
expect(screen.getByRole('button', { name: /import data/i })).toBeInTheDocument();
|
|
614
|
+
// Dialog has a close button too, so get all close buttons and find the Cancel one
|
|
615
|
+
const closeButtons = screen.getAllByRole('button', { name: /close/i });
|
|
616
|
+
const cancelButton = closeButtons.find(btn => btn.textContent === 'Close');
|
|
617
|
+
expect(cancelButton).toBeInTheDocument();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('uses custom preview header text from config', async () => {
|
|
621
|
+
const user = userEvent.setup();
|
|
622
|
+
const csvContent = 'name,email\nJohn,john@example.com';
|
|
623
|
+
const file = createCSVFile(csvContent);
|
|
624
|
+
|
|
625
|
+
render(
|
|
626
|
+
<ImportModal
|
|
627
|
+
{...defaultProps}
|
|
628
|
+
config={{ previewHeaderText: 'Data Preview' }}
|
|
629
|
+
/>
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
633
|
+
await user.upload(fileInput, file);
|
|
634
|
+
|
|
635
|
+
// Wait for preview to appear
|
|
636
|
+
await waitFor(() => {
|
|
637
|
+
expect(screen.getByText('Data Preview')).toBeInTheDocument();
|
|
638
|
+
}, { timeout: 5000 });
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('uses custom total rows text from config', async () => {
|
|
642
|
+
const user = userEvent.setup();
|
|
643
|
+
const csvContent = 'name,email\nJohn,john@example.com\nJane,jane@example.com';
|
|
644
|
+
const file = createCSVFile(csvContent);
|
|
645
|
+
|
|
646
|
+
render(
|
|
647
|
+
<ImportModal
|
|
648
|
+
{...defaultProps}
|
|
649
|
+
config={{ totalRowsText: 'Found {count} records' }}
|
|
650
|
+
/>
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
654
|
+
await user.upload(fileInput, file);
|
|
655
|
+
|
|
656
|
+
// Wait for preview to appear
|
|
657
|
+
await waitFor(() => {
|
|
658
|
+
const table = screen.queryByRole('table');
|
|
659
|
+
expect(table).toBeInTheDocument();
|
|
660
|
+
}, { timeout: 5000 });
|
|
661
|
+
|
|
662
|
+
await waitFor(() => {
|
|
663
|
+
expect(screen.getByText(/found 2 records/i)).toBeInTheDocument();
|
|
664
|
+
}, { timeout: 2000 });
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
describe('Edge Cases', () => {
|
|
669
|
+
it('handles empty CSV file', async () => {
|
|
670
|
+
const user = userEvent.setup();
|
|
671
|
+
const file = createCSVFile('');
|
|
672
|
+
|
|
673
|
+
render(<ImportModal {...defaultProps} />);
|
|
674
|
+
|
|
675
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
676
|
+
await user.upload(fileInput, file);
|
|
677
|
+
|
|
678
|
+
await waitFor(() => {
|
|
679
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
680
|
+
}, { timeout: 3000 });
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('handles CSV with only whitespace', async () => {
|
|
684
|
+
const user = userEvent.setup();
|
|
685
|
+
const file = createCSVFile(' \n \n ');
|
|
686
|
+
|
|
687
|
+
render(<ImportModal {...defaultProps} />);
|
|
688
|
+
|
|
689
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
690
|
+
await user.upload(fileInput, file);
|
|
691
|
+
|
|
692
|
+
await waitFor(() => {
|
|
693
|
+
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
|
694
|
+
}, { timeout: 3000 });
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('handles very large CSV files', async () => {
|
|
698
|
+
const user = userEvent.setup();
|
|
699
|
+
const headers = 'name,email\n';
|
|
700
|
+
const rows = Array.from({ length: 1000 }, (_, i) => `User${i},user${i}@example.com`).join('\n');
|
|
701
|
+
const csvContent = headers + rows;
|
|
702
|
+
const file = createCSVFile(csvContent);
|
|
703
|
+
|
|
704
|
+
render(<ImportModal {...defaultProps} />);
|
|
705
|
+
|
|
706
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
707
|
+
await user.upload(fileInput, file);
|
|
708
|
+
|
|
709
|
+
await waitFor(() => {
|
|
710
|
+
expect(screen.getByText(/total rows to import: 1000/i)).toBeInTheDocument();
|
|
711
|
+
}, { timeout: 5000 });
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe('Accessibility', () => {
|
|
716
|
+
it('provides accessible file input', () => {
|
|
717
|
+
render(<ImportModal {...defaultProps} />);
|
|
718
|
+
|
|
719
|
+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
720
|
+
expect(fileInput).toBeInTheDocument();
|
|
721
|
+
expect(fileInput).toHaveAttribute('type', 'file');
|
|
722
|
+
expect(fileInput).toHaveAttribute('accept', '.csv');
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('provides accessible button labels', () => {
|
|
726
|
+
render(<ImportModal {...defaultProps} />);
|
|
727
|
+
|
|
728
|
+
expect(screen.getByRole('button', { name: /select file/i })).toBeInTheDocument();
|
|
729
|
+
expect(screen.getByRole('button', { name: /import/i })).toBeInTheDocument();
|
|
730
|
+
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|