@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.
Files changed (236) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
  28. package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +43 -16
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +32 -17
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ImportModal.tsx +25 -2
  163. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  164. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  165. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  166. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  168. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  169. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  170. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  171. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  172. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  173. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  174. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  175. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  176. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  177. package/src/components/EventSelector/EventSelector.tsx +5 -25
  178. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  179. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  184. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  186. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  187. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  188. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  189. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  190. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  191. package/src/components/Select/Select.tsx +8 -0
  192. package/src/components/Toast/Toast.test.tsx +8 -7
  193. package/src/components/Toast/Toast.tsx +4 -4
  194. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  195. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  196. package/src/hooks/useEventTheme.ts +49 -18
  197. package/src/hooks/usePermissionCache.ts +5 -3
  198. package/src/hooks/useSecureDataAccess.ts +11 -1
  199. package/src/hooks/useToast.ts +11 -12
  200. package/src/providers/services/EventServiceProvider.tsx +15 -8
  201. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  202. package/src/rbac/audit.test.ts +206 -0
  203. package/src/rbac/audit.ts +37 -2
  204. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  205. package/src/rbac/errors.test.ts +340 -0
  206. package/src/rbac/hooks/index.ts +9 -0
  207. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  208. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  209. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  210. package/src/services/AuthService.ts +10 -0
  211. package/src/services/EventService.ts +111 -50
  212. package/src/services/__tests__/AuthService.test.ts +1 -1
  213. package/src/services/__tests__/EventService.test.ts +60 -45
  214. package/src/services/interfaces/IEventService.ts +1 -1
  215. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  216. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  217. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  218. package/src/utils/file-reference.test.ts +214 -0
  219. package/dist/chunk-3OGQLOJM.js.map +0 -1
  220. package/dist/chunk-4OX5PXHX.js.map +0 -1
  221. package/dist/chunk-5CDJCTOO.js +0 -190
  222. package/dist/chunk-5YIZFEUQ.js.map +0 -1
  223. package/dist/chunk-F6QB26OS.js.map +0 -1
  224. package/dist/chunk-KTHLNIMA.js.map +0 -1
  225. package/dist/chunk-OO3V7W4H.js.map +0 -1
  226. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  227. package/src/rbac/audit-enhanced.ts +0 -351
  228. /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  230. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  231. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  232. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  233. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  234. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  235. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  236. /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
+