@jmruthers/pace-core 0.5.117 → 0.5.119

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 (167) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
  2. package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
  3. package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
  4. package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
  5. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  6. package/dist/chunk-BHWIUEYH.js.map +1 -0
  7. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  8. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  9. package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  10. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  11. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  12. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  13. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  14. package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
  15. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  16. package/dist/components.js +8 -8
  17. package/dist/hooks.js +7 -7
  18. package/dist/index.js +11 -11
  19. package/dist/providers.js +2 -2
  20. package/dist/rbac/index.js +7 -7
  21. package/dist/utils.js +1 -1
  22. package/docs/api/classes/ColumnFactory.md +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/classes/StorageUtils.md +1 -1
  36. package/docs/api/enums/FileCategory.md +1 -1
  37. package/docs/api/interfaces/AggregateConfig.md +1 -1
  38. package/docs/api/interfaces/ButtonProps.md +1 -1
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  51. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  52. package/docs/api/interfaces/FileMetadata.md +1 -1
  53. package/docs/api/interfaces/FileReference.md +1 -1
  54. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  55. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  56. package/docs/api/interfaces/FileUploadProps.md +1 -1
  57. package/docs/api/interfaces/FooterProps.md +1 -1
  58. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  59. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  60. package/docs/api/interfaces/InputProps.md +1 -1
  61. package/docs/api/interfaces/LabelProps.md +1 -1
  62. package/docs/api/interfaces/LoginFormProps.md +1 -1
  63. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  64. package/docs/api/interfaces/NavigationContextType.md +1 -1
  65. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  66. package/docs/api/interfaces/NavigationItem.md +1 -1
  67. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  68. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  69. package/docs/api/interfaces/Organisation.md +1 -1
  70. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  71. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  72. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  73. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  74. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  75. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  76. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  77. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  78. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  79. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  80. package/docs/api/interfaces/PaletteData.md +1 -1
  81. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  82. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  84. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  85. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  88. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  89. package/docs/api/interfaces/RBACConfig.md +1 -1
  90. package/docs/api/interfaces/RBACLogger.md +1 -1
  91. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  93. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  94. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  95. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  96. package/docs/api/interfaces/RouteConfig.md +1 -1
  97. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  98. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  99. package/docs/api/interfaces/StorageConfig.md +1 -1
  100. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  101. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  102. package/docs/api/interfaces/StorageListOptions.md +1 -1
  103. package/docs/api/interfaces/StorageListResult.md +1 -1
  104. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  105. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  106. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  107. package/docs/api/interfaces/StyleImport.md +1 -1
  108. package/docs/api/interfaces/SwitchProps.md +1 -1
  109. package/docs/api/interfaces/ToastActionElement.md +1 -1
  110. package/docs/api/interfaces/ToastProps.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  112. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  114. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  116. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  118. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  119. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  121. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  122. package/docs/api/interfaces/UserEventAccess.md +1 -1
  123. package/docs/api/interfaces/UserMenuProps.md +1 -1
  124. package/docs/api/interfaces/UserProfile.md +1 -1
  125. package/docs/api/modules.md +2 -2
  126. package/package.json +1 -1
  127. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  128. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  129. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  130. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  131. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  132. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  133. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  134. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  135. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  136. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  137. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  138. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  139. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  140. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  141. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  142. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  143. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  144. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  145. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  146. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  147. package/src/hooks/useSecureDataAccess.ts +43 -5
  148. package/src/rbac/audit-enhanced.ts +339 -0
  149. package/src/services/EventService.ts +1 -0
  150. package/src/services/__tests__/AuthService.test.ts +473 -0
  151. package/src/services/__tests__/EventService.test.ts +390 -0
  152. package/src/services/__tests__/InactivityService.test.ts +217 -0
  153. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  154. package/dist/chunk-KA3PSVNV.js.map +0 -1
  155. package/src/components/DataTable/utils/debugTools.ts +0 -609
  156. package/src/rbac/testing/index.tsx +0 -340
  157. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  158. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  159. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  160. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  161. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  162. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  163. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  164. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  165. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  166. /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  167. /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
@@ -0,0 +1,1004 @@
1
+ /**
2
+ * @file UnifiedTableBody Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for UnifiedTableBody component following TEST_STANDARD.md.
8
+ * Tests cover rendering, virtualization, editing, hierarchical data, 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 { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
16
+ import { renderHook } from '@testing-library/react';
17
+ import { UnifiedTableBody } from '../UnifiedTableBody';
18
+ import type { DataRecord } from '../../types';
19
+ import { createTestData, createTestColumns } from '../../__tests__/test-utils/dataFactories';
20
+
21
+ // Mock Button component
22
+ vi.mock('../../Button/Button', () => ({
23
+ Button: ({ children, onClick, size, variant, className, 'aria-label': ariaLabel }: any) => (
24
+ <button
25
+ onClick={onClick}
26
+ data-size={size}
27
+ data-variant={variant}
28
+ className={className}
29
+ aria-label={ariaLabel}
30
+ >
31
+ {children}
32
+ </button>
33
+ ),
34
+ }));
35
+
36
+ // Mock Input component
37
+ vi.mock('../../Input/Input', () => ({
38
+ Input: React.forwardRef(({ type, value, onChange, className, placeholder }: any, ref: any) => (
39
+ <input
40
+ ref={ref}
41
+ type={type}
42
+ value={value}
43
+ onChange={onChange}
44
+ className={className}
45
+ placeholder={placeholder}
46
+ data-testid={`input-${type || 'text'}`}
47
+ />
48
+ )),
49
+ }));
50
+
51
+ // Mock Select components
52
+ vi.mock('../../Select/Select', () => ({
53
+ Select: React.forwardRef(({ children, value, onValueChange, onOpenChange }: any, ref: any) => (
54
+ <div ref={ref} data-testid="select" data-value={value}>
55
+ {children}
56
+ <button onClick={() => onOpenChange?.(true)} data-testid="select-open">Open</button>
57
+ <button onClick={() => onValueChange?.('option1')} data-testid="select-change">Change</button>
58
+ </div>
59
+ )),
60
+ SelectTrigger: ({ children, className }: any) => (
61
+ <div data-testid="select-trigger" className={className}>{children}</div>
62
+ ),
63
+ SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
64
+ SelectContent: ({ children, searchable, searchPlaceholder }: any) => (
65
+ <div data-testid="select-content" data-searchable={searchable} data-placeholder={searchPlaceholder}>
66
+ {children}
67
+ </div>
68
+ ),
69
+ SelectItem: ({ children, value }: any) => (
70
+ <div data-testid="select-item" data-value={value}>{children}</div>
71
+ ),
72
+ SelectGroup: ({ children }: any) => <div data-testid="select-group">{children}</div>,
73
+ SelectLabel: ({ children }: any) => <div data-testid="select-label">{children}</div>,
74
+ SelectSeparator: () => <div data-testid="select-separator" />,
75
+ }));
76
+
77
+ // Mock lucide-react icons
78
+ vi.mock('lucide-react', async () => {
79
+ const actual = await vi.importActual('lucide-react');
80
+ return {
81
+ ...actual,
82
+ ChevronUp: ({ className }: { className?: string }) => <span className={className} data-testid="chevron-up">↑</span>,
83
+ ChevronDown: ({ className }: { className?: string }) => <span className={className} data-testid="chevron-down">↓</span>,
84
+ ChevronRight: ({ className }: { className?: string }) => <span className={className} data-testid="chevron-right">→</span>,
85
+ Check: ({ className }: { className?: string }) => <span className={className} data-testid="check-icon">✓</span>,
86
+ };
87
+ });
88
+
89
+ // Mock EmptyState component
90
+ vi.mock('../EmptyState', () => ({
91
+ EmptyState: ({ title, description, isFiltered, onClearFilters }: any) => (
92
+ <div data-testid="empty-state">
93
+ {title && <div>{title}</div>}
94
+ {description && <div>{description}</div>}
95
+ {isFiltered && onClearFilters && (
96
+ <button onClick={onClearFilters} data-testid="clear-filters">Clear Filters</button>
97
+ )}
98
+ </div>
99
+ ),
100
+ }));
101
+
102
+ // Mock FilterRow component
103
+ vi.mock('../FilterRow', () => ({
104
+ FilterRow: ({ table }: any) => <div data-testid="filter-row">Filter Row</div>,
105
+ }));
106
+
107
+ // Mock ActionButtons component
108
+ vi.mock('../ActionButtons', () => ({
109
+ ActionButtons: ({ row, actions, isEditing }: any) => (
110
+ <div data-testid="action-buttons">
111
+ {actions?.map((action: any, idx: number) => (
112
+ <button key={idx} onClick={() => action.onClick?.(row.original)} data-testid={`action-${action.testId || idx}`}>
113
+ {action.label}
114
+ </button>
115
+ ))}
116
+ </div>
117
+ ),
118
+ }));
119
+
120
+ // Mock EditableRow component
121
+ vi.mock('../EditableRow', () => ({
122
+ EditableRow: ({ row, editingData, onEditingDataChange, onSave, onCancel }: any) => (
123
+ <tr data-testid="editable-row">
124
+ <td>
125
+ <input
126
+ data-testid="editable-input"
127
+ value={editingData?.name || ''}
128
+ onChange={(e) => onEditingDataChange?.({ ...editingData, name: e.target.value })}
129
+ />
130
+ <button onClick={onSave} data-testid="save-editable">Save</button>
131
+ <button onClick={onCancel} data-testid="cancel-editable">Cancel</button>
132
+ </td>
133
+ </tr>
134
+ ),
135
+ }));
136
+
137
+ // Mock virtualizer
138
+ vi.mock('@tanstack/react-virtual', () => ({
139
+ useVirtualizer: vi.fn(() => ({
140
+ getVirtualItems: vi.fn(() => []),
141
+ getTotalSize: vi.fn(() => 0),
142
+ scrollToIndex: vi.fn(),
143
+ scrollToOffset: vi.fn(),
144
+ })),
145
+ }));
146
+
147
+ interface TestData extends DataRecord {
148
+ id: string;
149
+ name: string;
150
+ email: string;
151
+ age: number;
152
+ }
153
+
154
+ describe('[component] UnifiedTableBody', () => {
155
+ const mockData = createTestData(5);
156
+ const mockColumns = createTestColumns();
157
+
158
+ const createMockTable = (data: any[] = mockData, columns: any[] = mockColumns) => {
159
+ const { result } = renderHook(() =>
160
+ useReactTable({
161
+ data,
162
+ columns,
163
+ getCoreRowModel: getCoreRowModel(),
164
+ })
165
+ );
166
+ return result.current;
167
+ };
168
+
169
+ const defaultProps = {
170
+ table: createMockTable(),
171
+ isCreating: false,
172
+ creationData: {},
173
+ onCreationDataChange: vi.fn(),
174
+ onSaveCreation: vi.fn(),
175
+ onCancelCreation: vi.fn(),
176
+ editingRowId: null,
177
+ editingData: {},
178
+ onEditingDataChange: vi.fn(),
179
+ onSaveEditing: vi.fn(),
180
+ onCancelEditing: vi.fn(),
181
+ grouping: [],
182
+ aggregates: [],
183
+ getRowId: (row: any) => row.id || String(row.name),
184
+ dataLength: mockData.length,
185
+ virtualHeight: 600,
186
+ forceVirtualization: false,
187
+ actions: [],
188
+ rbac: { pageId: 'test-page' },
189
+ permissions: {
190
+ canRead: { can: true, isLoading: false },
191
+ canCreate: { can: true, isLoading: false },
192
+ canUpdate: { can: true, isLoading: false },
193
+ canDelete: { can: true, isLoading: false },
194
+ canExport: { can: true, isLoading: false },
195
+ canImport: { can: true, isLoading: false },
196
+ },
197
+ };
198
+
199
+ beforeEach(() => {
200
+ vi.clearAllMocks();
201
+ // Suppress console.log for cleaner test output
202
+ vi.spyOn(console, 'log').mockImplementation(() => {});
203
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
204
+ });
205
+
206
+ afterEach(() => {
207
+ vi.restoreAllMocks();
208
+ vi.clearAllMocks();
209
+ });
210
+
211
+ describe('Rendering', () => {
212
+ it('renders table body with rows', () => {
213
+ const table = createMockTable();
214
+ render(
215
+ <table>
216
+ <UnifiedTableBody {...defaultProps} table={table} />
217
+ </table>
218
+ );
219
+
220
+ const tbody = screen.getByRole('rowgroup');
221
+ expect(tbody).toBeInTheDocument();
222
+ });
223
+
224
+ it('renders empty state when no rows', () => {
225
+ const table = createMockTable([]);
226
+ render(
227
+ <table>
228
+ <UnifiedTableBody {...defaultProps} table={table} dataLength={0} />
229
+ </table>
230
+ );
231
+
232
+ expect(screen.getByTestId('empty-state')).toBeInTheDocument();
233
+ });
234
+
235
+ it('renders custom empty state configuration', () => {
236
+ const table = createMockTable([]);
237
+ const emptyState = {
238
+ title: 'No Records',
239
+ description: 'Add some data',
240
+ };
241
+ render(
242
+ <table>
243
+ <UnifiedTableBody
244
+ {...defaultProps}
245
+ table={table}
246
+ dataLength={0}
247
+ emptyState={emptyState}
248
+ />
249
+ </table>
250
+ );
251
+
252
+ expect(screen.getByText('No Records')).toBeInTheDocument();
253
+ expect(screen.getByText('Add some data')).toBeInTheDocument();
254
+ });
255
+
256
+ it('renders filter row when enabled', () => {
257
+ const table = createMockTable();
258
+ render(
259
+ <table>
260
+ <UnifiedTableBody
261
+ {...defaultProps}
262
+ table={table}
263
+ enableFiltering={true}
264
+ showFilterRow={true}
265
+ />
266
+ </table>
267
+ );
268
+
269
+ expect(screen.getByTestId('filter-row')).toBeInTheDocument();
270
+ });
271
+ });
272
+
273
+ describe('Row Creation', () => {
274
+ it('renders creation row when isCreating is true', () => {
275
+ const table = createMockTable();
276
+ render(
277
+ <table>
278
+ <UnifiedTableBody
279
+ {...defaultProps}
280
+ table={table}
281
+ isCreating={true}
282
+ creationData={{ name: 'New User', email: 'new@example.com' }}
283
+ />
284
+ </table>
285
+ );
286
+
287
+ // Check that inputs are rendered (by placeholder or value)
288
+ const nameInput = screen.getByDisplayValue('New User');
289
+ expect(nameInput).toBeInTheDocument();
290
+ });
291
+
292
+ it('calls onCreationDataChange when creation input changes', async () => {
293
+ const user = userEvent.setup();
294
+ const onCreationDataChange = vi.fn();
295
+ const table = createMockTable();
296
+
297
+ render(
298
+ <table>
299
+ <UnifiedTableBody
300
+ {...defaultProps}
301
+ table={table}
302
+ isCreating={true}
303
+ creationData={{ name: '' }}
304
+ onCreationDataChange={onCreationDataChange}
305
+ />
306
+ </table>
307
+ );
308
+
309
+ // Find input by placeholder since mock might not have testid
310
+ const input = screen.getByPlaceholderText(/Enter Name/i);
311
+ await user.type(input, 'New Name');
312
+
313
+ expect(onCreationDataChange).toHaveBeenCalled();
314
+ });
315
+
316
+ it('calls onSaveCreation when save button is clicked', async () => {
317
+ const user = userEvent.setup();
318
+ const onSaveCreation = vi.fn();
319
+ const table = createMockTable();
320
+
321
+ render(
322
+ <table>
323
+ <UnifiedTableBody
324
+ {...defaultProps}
325
+ table={table}
326
+ isCreating={true}
327
+ creationData={{ name: 'New User' }}
328
+ onSaveCreation={onSaveCreation}
329
+ />
330
+ </table>
331
+ );
332
+
333
+ const saveButton = screen.getByTitle('Save new row');
334
+ await user.click(saveButton);
335
+
336
+ expect(onSaveCreation).toHaveBeenCalledTimes(1);
337
+ });
338
+
339
+ it('calls onCancelCreation when cancel button is clicked', async () => {
340
+ const user = userEvent.setup();
341
+ const onCancelCreation = vi.fn();
342
+ const table = createMockTable();
343
+
344
+ render(
345
+ <table>
346
+ <UnifiedTableBody
347
+ {...defaultProps}
348
+ table={table}
349
+ isCreating={true}
350
+ creationData={{ name: 'New User' }}
351
+ onCancelCreation={onCancelCreation}
352
+ />
353
+ </table>
354
+ );
355
+
356
+ const cancelButton = screen.getByTitle('Cancel new row');
357
+ await user.click(cancelButton);
358
+
359
+ expect(onCancelCreation).toHaveBeenCalledTimes(1);
360
+ });
361
+
362
+ it('renders select field for select column type in creation mode', () => {
363
+ const columnsWithSelect = [
364
+ {
365
+ id: 'status',
366
+ header: 'Status',
367
+ accessorKey: 'status',
368
+ fieldType: 'select' as const,
369
+ fieldOptions: [
370
+ { value: 'active', label: 'Active' },
371
+ { value: 'inactive', label: 'Inactive' },
372
+ ],
373
+ },
374
+ ];
375
+ const table = createMockTable(mockData, columnsWithSelect);
376
+
377
+ render(
378
+ <table>
379
+ <UnifiedTableBody
380
+ {...defaultProps}
381
+ table={table}
382
+ isCreating={true}
383
+ creationData={{ status: 'active' }}
384
+ />
385
+ </table>
386
+ );
387
+
388
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
389
+ });
390
+
391
+ it('renders number input for number field type in creation mode', () => {
392
+ const columnsWithNumber = [
393
+ {
394
+ id: 'age',
395
+ header: 'Age',
396
+ accessorKey: 'age',
397
+ fieldType: 'number' as const,
398
+ },
399
+ ];
400
+ const table = createMockTable(mockData, columnsWithNumber);
401
+
402
+ render(
403
+ <table>
404
+ <UnifiedTableBody
405
+ {...defaultProps}
406
+ table={table}
407
+ isCreating={true}
408
+ creationData={{ age: '25' }}
409
+ />
410
+ </table>
411
+ );
412
+
413
+ // Find by type since the actual Input component is rendered
414
+ const numberInput = screen.getByDisplayValue('25');
415
+ expect(numberInput).toHaveAttribute('type', 'number');
416
+ });
417
+
418
+ it('renders date input for date field type in creation mode', () => {
419
+ const columnsWithDate = [
420
+ {
421
+ id: 'createdAt',
422
+ header: 'Created At',
423
+ accessorKey: 'createdAt',
424
+ fieldType: 'date' as const,
425
+ },
426
+ ];
427
+ const table = createMockTable(mockData, columnsWithDate);
428
+
429
+ render(
430
+ <table>
431
+ <UnifiedTableBody
432
+ {...defaultProps}
433
+ table={table}
434
+ isCreating={true}
435
+ creationData={{ createdAt: '2024-01-01' }}
436
+ />
437
+ </table>
438
+ );
439
+
440
+ // Find by type and value since the actual Input component is rendered
441
+ const dateInput = screen.getByDisplayValue('2024-01-01');
442
+ expect(dateInput).toHaveAttribute('type', 'date');
443
+ });
444
+ });
445
+
446
+ describe('Row Editing', () => {
447
+ it('renders EditableRow when row is being edited', () => {
448
+ const table = createMockTable();
449
+ const rowId = mockData[0].id;
450
+
451
+ render(
452
+ <table>
453
+ <UnifiedTableBody
454
+ {...defaultProps}
455
+ table={table}
456
+ editingRowId={rowId}
457
+ editingData={{ name: 'Edited Name' }}
458
+ />
459
+ </table>
460
+ );
461
+
462
+ expect(screen.getByTestId('editable-row')).toBeInTheDocument();
463
+ });
464
+
465
+ it('calls onEditingDataChange when editing data changes', async () => {
466
+ const user = userEvent.setup();
467
+ const onEditingDataChange = vi.fn();
468
+ const table = createMockTable();
469
+ const rowId = mockData[0].id;
470
+
471
+ render(
472
+ <table>
473
+ <UnifiedTableBody
474
+ {...defaultProps}
475
+ table={table}
476
+ editingRowId={rowId}
477
+ editingData={{ name: 'Current Name' }}
478
+ onEditingDataChange={onEditingDataChange}
479
+ />
480
+ </table>
481
+ );
482
+
483
+ const editableInput = screen.getByTestId('editable-input');
484
+ await user.clear(editableInput);
485
+ await user.type(editableInput, 'New Name');
486
+
487
+ expect(onEditingDataChange).toHaveBeenCalled();
488
+ });
489
+
490
+ it('calls onSaveEditing when save button is clicked in edit mode', async () => {
491
+ const user = userEvent.setup();
492
+ const onSaveEditing = vi.fn();
493
+ const table = createMockTable();
494
+ const rowId = mockData[0].id;
495
+
496
+ render(
497
+ <table>
498
+ <UnifiedTableBody
499
+ {...defaultProps}
500
+ table={table}
501
+ editingRowId={rowId}
502
+ editingData={{ name: 'Edited Name' }}
503
+ onSaveEditing={onSaveEditing}
504
+ />
505
+ </table>
506
+ );
507
+
508
+ const saveButton = screen.getByTestId('save-editable');
509
+ await user.click(saveButton);
510
+
511
+ expect(onSaveEditing).toHaveBeenCalledTimes(1);
512
+ });
513
+
514
+ it('calls onCancelEditing when cancel button is clicked in edit mode', async () => {
515
+ const user = userEvent.setup();
516
+ const onCancelEditing = vi.fn();
517
+ const table = createMockTable();
518
+ const rowId = mockData[0].id;
519
+
520
+ render(
521
+ <table>
522
+ <UnifiedTableBody
523
+ {...defaultProps}
524
+ table={table}
525
+ editingRowId={rowId}
526
+ editingData={{ name: 'Edited Name' }}
527
+ onCancelEditing={onCancelEditing}
528
+ />
529
+ </table>
530
+ );
531
+
532
+ const cancelButton = screen.getByTestId('cancel-editable');
533
+ await user.click(cancelButton);
534
+
535
+ expect(onCancelEditing).toHaveBeenCalledTimes(1);
536
+ });
537
+ });
538
+
539
+ describe('Grouped Rows', () => {
540
+ it('renders group header row when grouping is enabled', () => {
541
+ const table = createMockTable();
542
+ // Mock grouped row
543
+ const mockGroupedRow = {
544
+ ...table.getRowModel().rows[0],
545
+ getIsGrouped: () => true,
546
+ getValue: (columnId: string) => 'Group A',
547
+ subRows: table.getRowModel().rows,
548
+ toggleExpanded: vi.fn(),
549
+ getIsExpanded: () => false,
550
+ };
551
+
552
+ const tableWithGrouped = {
553
+ ...table,
554
+ getRowModel: () => ({
555
+ rows: [mockGroupedRow],
556
+ }),
557
+ };
558
+
559
+ render(
560
+ <table>
561
+ <UnifiedTableBody
562
+ {...defaultProps}
563
+ table={tableWithGrouped as any}
564
+ grouping={['department']}
565
+ />
566
+ </table>
567
+ );
568
+
569
+ // Group header should be rendered
570
+ expect(screen.getByText(/Group A/)).toBeInTheDocument();
571
+ });
572
+
573
+ it('toggles group expansion when group header is clicked', async () => {
574
+ const user = userEvent.setup();
575
+ const toggleExpanded = vi.fn();
576
+ const table = createMockTable();
577
+ const mockGroupedRow = {
578
+ ...table.getRowModel().rows[0],
579
+ getIsGrouped: () => true,
580
+ getValue: (columnId: string) => 'Group A',
581
+ subRows: table.getRowModel().rows,
582
+ toggleExpanded,
583
+ getIsExpanded: () => false,
584
+ };
585
+
586
+ const tableWithGrouped = {
587
+ ...table,
588
+ getRowModel: () => ({
589
+ rows: [mockGroupedRow],
590
+ }),
591
+ };
592
+
593
+ render(
594
+ <table>
595
+ <UnifiedTableBody
596
+ {...defaultProps}
597
+ table={tableWithGrouped as any}
598
+ grouping={['department']}
599
+ />
600
+ </table>
601
+ );
602
+
603
+ const expandButton = screen.getByRole('button');
604
+ await user.click(expandButton);
605
+
606
+ expect(toggleExpanded).toHaveBeenCalled();
607
+ });
608
+ });
609
+
610
+ describe('Hierarchical Data', () => {
611
+ it('renders hierarchical rows with indentation', () => {
612
+ const hierarchicalData = [
613
+ { id: '1', name: 'Parent', isParent: true },
614
+ { id: '2', name: 'Child', isParent: false, parentId: '1' },
615
+ ];
616
+ const table = createMockTable(hierarchicalData as any);
617
+
618
+ const hierarchicalState = {
619
+ isExpanded: vi.fn((id: string) => id === '1'),
620
+ hasChildren: vi.fn((id: string) => id === '1'),
621
+ getChildrenCount: vi.fn((id: string) => id === '1' ? 1 : 0),
622
+ toggleRow: vi.fn(),
623
+ };
624
+
625
+ render(
626
+ <table>
627
+ <UnifiedTableBody
628
+ {...defaultProps}
629
+ table={table}
630
+ hierarchical={{
631
+ enabled: true,
632
+ indentSize: 24,
633
+ state: hierarchicalState,
634
+ }}
635
+ />
636
+ </table>
637
+ );
638
+
639
+ const tbody = screen.getByRole('rowgroup');
640
+ expect(tbody).toBeInTheDocument();
641
+ });
642
+
643
+ it('renders expansion button for parent rows', () => {
644
+ const hierarchicalData = [
645
+ { id: '1', name: 'Parent', isParent: true },
646
+ ];
647
+ const table = createMockTable(hierarchicalData as any);
648
+
649
+ const hierarchicalState = {
650
+ isExpanded: vi.fn(() => false),
651
+ hasChildren: vi.fn(() => true),
652
+ getChildrenCount: vi.fn(() => 1),
653
+ toggleRow: vi.fn(),
654
+ };
655
+
656
+ render(
657
+ <table>
658
+ <UnifiedTableBody
659
+ {...defaultProps}
660
+ table={table}
661
+ hierarchical={{
662
+ enabled: true,
663
+ indentSize: 24,
664
+ state: hierarchicalState,
665
+ }}
666
+ />
667
+ </table>
668
+ );
669
+
670
+ // Expansion button should be rendered (ChevronRight icon)
671
+ const buttons = screen.getAllByRole('button');
672
+ expect(buttons.length).toBeGreaterThan(0);
673
+ });
674
+ });
675
+
676
+ describe('Action Buttons', () => {
677
+ it('renders action buttons for each row when actions column exists', () => {
678
+ // Create table with actions column
679
+ const columnsWithActions = [
680
+ ...mockColumns,
681
+ {
682
+ id: 'actions',
683
+ header: 'Actions',
684
+ cell: () => null,
685
+ },
686
+ ];
687
+ const table = createMockTable(mockData, columnsWithActions);
688
+ const actions = [
689
+ { label: 'Edit', onClick: vi.fn(), testId: 'edit' },
690
+ { label: 'Delete', onClick: vi.fn(), testId: 'delete' },
691
+ ];
692
+
693
+ render(
694
+ <table>
695
+ <UnifiedTableBody
696
+ {...defaultProps}
697
+ table={table}
698
+ actions={actions}
699
+ />
700
+ </table>
701
+ );
702
+
703
+ // ActionButtons mock renders with action- prefix
704
+ // Multiple rows mean multiple buttons, so use getAllByTestId
705
+ const editButtons = screen.getAllByTestId('action-edit');
706
+ const deleteButtons = screen.getAllByTestId('action-delete');
707
+ expect(editButtons.length).toBeGreaterThan(0);
708
+ expect(deleteButtons.length).toBeGreaterThan(0);
709
+ });
710
+
711
+ it('calls action onClick when action button is clicked', async () => {
712
+ const user = userEvent.setup();
713
+ const onEditClick = vi.fn();
714
+ // Create table with actions column
715
+ const columnsWithActions = [
716
+ ...mockColumns,
717
+ {
718
+ id: 'actions',
719
+ header: 'Actions',
720
+ cell: () => null,
721
+ },
722
+ ];
723
+ const table = createMockTable(mockData, columnsWithActions);
724
+ const actions = [
725
+ { label: 'Edit', onClick: onEditClick, testId: 'edit' },
726
+ ];
727
+
728
+ render(
729
+ <table>
730
+ <UnifiedTableBody
731
+ {...defaultProps}
732
+ table={table}
733
+ actions={actions}
734
+ />
735
+ </table>
736
+ );
737
+
738
+ // ActionButtons mock renders with action- prefix
739
+ // Multiple rows mean multiple buttons, so use getAllByTestId and click the first one
740
+ const editButtons = screen.getAllByTestId('action-edit');
741
+ expect(editButtons.length).toBeGreaterThan(0);
742
+ await user.click(editButtons[0]);
743
+
744
+ expect(onEditClick).toHaveBeenCalled();
745
+ });
746
+ });
747
+
748
+ describe('Empty State', () => {
749
+ it('shows clear filters button when filtered', async () => {
750
+ const user = userEvent.setup();
751
+ const onClearFilters = vi.fn();
752
+ const table = createMockTable([]);
753
+
754
+ render(
755
+ <table>
756
+ <UnifiedTableBody
757
+ {...defaultProps}
758
+ table={table}
759
+ dataLength={0}
760
+ isFiltered={true}
761
+ onClearFilters={onClearFilters}
762
+ />
763
+ </table>
764
+ );
765
+
766
+ const clearButton = screen.getByTestId('clear-filters');
767
+ expect(clearButton).toBeInTheDocument();
768
+
769
+ await user.click(clearButton);
770
+ expect(onClearFilters).toHaveBeenCalledTimes(1);
771
+ });
772
+ });
773
+
774
+ describe('Virtualization', () => {
775
+ it('renders standard rows when virtualization is not needed', () => {
776
+ const table = createMockTable();
777
+ render(
778
+ <table>
779
+ <UnifiedTableBody
780
+ {...defaultProps}
781
+ table={table}
782
+ dataLength={5}
783
+ forceVirtualization={false}
784
+ />
785
+ </table>
786
+ );
787
+
788
+ const tbody = screen.getByRole('rowgroup');
789
+ expect(tbody).toBeInTheDocument();
790
+ });
791
+
792
+ it('uses virtualization when data length exceeds threshold', () => {
793
+ const largeData = createTestData(1500);
794
+ const table = createMockTable(largeData);
795
+
796
+ render(
797
+ <table>
798
+ <UnifiedTableBody
799
+ {...defaultProps}
800
+ table={table}
801
+ dataLength={1500}
802
+ forceVirtualization={false}
803
+ />
804
+ </table>
805
+ );
806
+
807
+ const tbody = screen.getByRole('rowgroup');
808
+ expect(tbody).toBeInTheDocument();
809
+ });
810
+
811
+ it('forces virtualization when forceVirtualization is true', () => {
812
+ const table = createMockTable();
813
+
814
+ render(
815
+ <table>
816
+ <UnifiedTableBody
817
+ {...defaultProps}
818
+ table={table}
819
+ dataLength={5}
820
+ forceVirtualization={true}
821
+ />
822
+ </table>
823
+ );
824
+
825
+ const tbody = screen.getByRole('rowgroup');
826
+ expect(tbody).toBeInTheDocument();
827
+ });
828
+ });
829
+
830
+ describe('Edge Cases', () => {
831
+ it('handles null values in creationData gracefully', () => {
832
+ const table = createMockTable();
833
+ render(
834
+ <table>
835
+ <UnifiedTableBody
836
+ {...defaultProps}
837
+ table={table}
838
+ isCreating={true}
839
+ creationData={{ name: null as any, email: null as any }}
840
+ />
841
+ </table>
842
+ );
843
+
844
+ const tbody = screen.getByRole('rowgroup');
845
+ expect(tbody).toBeInTheDocument();
846
+ });
847
+
848
+ it('handles missing getRowId function', () => {
849
+ const table = createMockTable();
850
+ render(
851
+ <table>
852
+ <UnifiedTableBody
853
+ {...defaultProps}
854
+ table={table}
855
+ getRowId={undefined}
856
+ />
857
+ </table>
858
+ );
859
+
860
+ const tbody = screen.getByRole('rowgroup');
861
+ expect(tbody).toBeInTheDocument();
862
+ });
863
+
864
+ it('handles empty actions array', () => {
865
+ const table = createMockTable();
866
+ render(
867
+ <table>
868
+ <UnifiedTableBody
869
+ {...defaultProps}
870
+ table={table}
871
+ actions={[]}
872
+ />
873
+ </table>
874
+ );
875
+
876
+ const tbody = screen.getByRole('rowgroup');
877
+ expect(tbody).toBeInTheDocument();
878
+ });
879
+
880
+ it('handles columns with editAccessorKey', () => {
881
+ const columnsWithEditKey = [
882
+ {
883
+ id: 'name',
884
+ header: 'Name',
885
+ accessorKey: 'name',
886
+ editAccessorKey: 'fullName',
887
+ },
888
+ ];
889
+ const table = createMockTable(mockData, columnsWithEditKey);
890
+
891
+ render(
892
+ <table>
893
+ <UnifiedTableBody
894
+ {...defaultProps}
895
+ table={table}
896
+ isCreating={true}
897
+ creationData={{ fullName: 'New Name' }}
898
+ />
899
+ </table>
900
+ );
901
+
902
+ const tbody = screen.getByRole('rowgroup');
903
+ expect(tbody).toBeInTheDocument();
904
+ });
905
+ });
906
+
907
+ describe('SelectEditField Component', () => {
908
+ it('renders creatable select with create option when search term has no match', async () => {
909
+ const user = userEvent.setup();
910
+ const columnsWithCreatable = [
911
+ {
912
+ id: 'status',
913
+ header: 'Status',
914
+ fieldType: 'select' as const,
915
+ fieldOptions: [
916
+ { value: 'active', label: 'Active' },
917
+ { value: 'inactive', label: 'Inactive' },
918
+ ],
919
+ creatable: true,
920
+ onCreateNew: vi.fn().mockResolvedValue('new-status'),
921
+ },
922
+ ];
923
+ const table = createMockTable(mockData, columnsWithCreatable);
924
+
925
+ render(
926
+ <table>
927
+ <UnifiedTableBody
928
+ {...defaultProps}
929
+ table={table}
930
+ isCreating={true}
931
+ creationData={{ status: '' }}
932
+ />
933
+ </table>
934
+ );
935
+
936
+ const selectTrigger = screen.getByTestId('select-trigger');
937
+ expect(selectTrigger).toBeInTheDocument();
938
+ });
939
+
940
+ it('renders select with groups', () => {
941
+ const columnsWithGroups = [
942
+ {
943
+ id: 'category',
944
+ header: 'Category',
945
+ fieldType: 'select' as const,
946
+ fieldOptions: [
947
+ {
948
+ type: 'group',
949
+ label: 'Group 1',
950
+ items: [
951
+ { value: 'option1', label: 'Option 1' },
952
+ { value: 'option2', label: 'Option 2' },
953
+ ],
954
+ },
955
+ ],
956
+ },
957
+ ];
958
+ const table = createMockTable(mockData, columnsWithGroups);
959
+
960
+ render(
961
+ <table>
962
+ <UnifiedTableBody
963
+ {...defaultProps}
964
+ table={table}
965
+ isCreating={true}
966
+ creationData={{ category: '' }}
967
+ />
968
+ </table>
969
+ );
970
+
971
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
972
+ });
973
+
974
+ it('renders select with separators', () => {
975
+ const columnsWithSeparators = [
976
+ {
977
+ id: 'status',
978
+ header: 'Status',
979
+ fieldType: 'select' as const,
980
+ fieldOptions: [
981
+ { value: 'active', label: 'Active' },
982
+ { type: 'separator' },
983
+ { value: 'inactive', label: 'Inactive' },
984
+ ],
985
+ },
986
+ ];
987
+ const table = createMockTable(mockData, columnsWithSeparators);
988
+
989
+ render(
990
+ <table>
991
+ <UnifiedTableBody
992
+ {...defaultProps}
993
+ table={table}
994
+ isCreating={true}
995
+ creationData={{ status: '' }}
996
+ />
997
+ </table>
998
+ );
999
+
1000
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
1001
+ });
1002
+ });
1003
+ });
1004
+