@jmruthers/pace-core 0.5.115 → 0.5.117

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 (235) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-H5KJCAIS.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-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
  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-HKWQN44G.js → chunk-IZXS7RZK.js} +15 -15
  15. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  16. package/dist/chunk-KA3PSVNV.js.map +1 -0
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  19. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  20. package/dist/chunk-P3PUOL6B.js.map +1 -0
  21. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  22. package/dist/chunk-PHDAXDHB.js.map +1 -0
  23. package/dist/chunk-UJI6WSMD.js +201 -0
  24. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  25. package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
  26. package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
  27. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  28. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  29. package/dist/{chunk-NEONKMTU.js → chunk-XN2LYHDI.js} +47 -6
  30. package/dist/chunk-XN2LYHDI.js.map +1 -0
  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-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
  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 +41 -14
  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 +29 -2
  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/ViewRowModal.tsx +1 -1
  163. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  164. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  165. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  166. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  167. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  168. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  169. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  170. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  171. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  172. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  173. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  174. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  175. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  176. package/src/components/EventSelector/EventSelector.tsx +5 -25
  177. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  178. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  179. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  184. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  185. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  186. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  187. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  188. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  189. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  190. package/src/components/Select/Select.tsx +8 -0
  191. package/src/components/Toast/Toast.tsx +1 -1
  192. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  193. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  194. package/src/hooks/useEventTheme.ts +49 -18
  195. package/src/hooks/usePermissionCache.ts +5 -3
  196. package/src/hooks/useSecureDataAccess.ts +56 -3
  197. package/src/hooks/useToast.ts +1 -1
  198. package/src/providers/services/EventServiceProvider.tsx +15 -8
  199. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  200. package/src/rbac/audit.test.ts +206 -0
  201. package/src/rbac/audit.ts +37 -2
  202. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  203. package/src/rbac/errors.test.ts +340 -0
  204. package/src/rbac/hooks/index.ts +9 -0
  205. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  206. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  207. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  208. package/src/services/AuthService.ts +10 -0
  209. package/src/services/EventService.ts +111 -50
  210. package/src/services/__tests__/AuthService.test.ts +1 -1
  211. package/src/services/__tests__/EventService.test.ts +60 -45
  212. package/src/services/interfaces/IEventService.ts +1 -1
  213. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  214. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  215. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  216. package/src/utils/file-reference.test.ts +214 -0
  217. package/dist/chunk-3OGQLOJM.js.map +0 -1
  218. package/dist/chunk-5CDJCTOO.js +0 -190
  219. package/dist/chunk-F6QB26OS.js.map +0 -1
  220. package/dist/chunk-KTHLNIMA.js.map +0 -1
  221. package/dist/chunk-NEONKMTU.js.map +0 -1
  222. package/dist/chunk-OO3V7W4H.js.map +0 -1
  223. package/dist/chunk-SYXOZQ4P.js.map +0 -1
  224. package/dist/chunk-XYRZV7R5.js.map +0 -1
  225. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  226. package/src/rbac/audit-enhanced.ts +0 -351
  227. /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  228. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  229. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  230. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  231. /package/dist/{chunk-HKWQN44G.js.map → chunk-IZXS7RZK.js.map} +0 -0
  232. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  233. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  234. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  235. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -0,0 +1,454 @@
1
+ /**
2
+ * @file Editable Row Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for EditableRow component following testing guidelines.
8
+ * Tests cover observable behavior and user interactions.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, renderHook } from '@testing-library/react';
13
+ import userEvent from '@testing-library/user-event';
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { createColumnHelper, useReactTable, getCoreRowModel } from '@tanstack/react-table';
16
+ import { EditableRow } from '../EditableRow';
17
+ import type { DataRecord } from '../../types';
18
+
19
+ // Mock Button component
20
+ vi.mock('../../Button/Button', () => ({
21
+ Button: ({ children, onClick, size, variant, 'aria-label': ariaLabel }: any) => (
22
+ <button
23
+ onClick={onClick}
24
+ data-size={size}
25
+ data-variant={variant}
26
+ aria-label={ariaLabel}
27
+ >
28
+ {children}
29
+ </button>
30
+ ),
31
+ }));
32
+
33
+ // Mock Input component
34
+ vi.mock('../../Input/Input', () => ({
35
+ Input: React.forwardRef(({ type, value, onChange, className, placeholder }: any, ref: any) => (
36
+ <input
37
+ ref={ref}
38
+ type={type}
39
+ value={value}
40
+ onChange={onChange}
41
+ className={className}
42
+ placeholder={placeholder}
43
+ />
44
+ )),
45
+ }));
46
+
47
+ // Mock Select components
48
+ vi.mock('../../Select/Select', () => ({
49
+ Select: ({ children, value, onValueChange, onOpenChange }: any) => (
50
+ <div data-testid="select" data-value={value}>
51
+ {children}
52
+ <button onClick={() => onOpenChange(true)} data-testid="select-open">Open</button>
53
+ <button onClick={() => onValueChange('option1')} data-testid="select-change">Change</button>
54
+ </div>
55
+ ),
56
+ SelectTrigger: ({ children, className }: any) => (
57
+ <div data-testid="select-trigger" className={className}>{children}</div>
58
+ ),
59
+ SelectValue: ({ placeholder }: any) => <div data-testid="select-value">{placeholder}</div>,
60
+ SelectContent: ({ children, searchable, searchPlaceholder }: any) => (
61
+ <div data-testid="select-content" data-searchable={searchable} data-placeholder={searchPlaceholder}>
62
+ {children}
63
+ </div>
64
+ ),
65
+ SelectItem: ({ children, value }: any) => (
66
+ <div data-testid="select-item" data-value={value}>{children}</div>
67
+ ),
68
+ SelectGroup: ({ children }: any) => <div data-testid="select-group">{children}</div>,
69
+ SelectLabel: ({ children }: any) => <div data-testid="select-label">{children}</div>,
70
+ SelectSeparator: () => <div data-testid="select-separator" />,
71
+ }));
72
+
73
+ // Mock lucide-react icons - need to include ChevronDown for Select
74
+ vi.mock('lucide-react', async () => {
75
+ const actual = await vi.importActual('lucide-react');
76
+ return {
77
+ ...actual,
78
+ X: ({ className }: { className?: string }) => (
79
+ <span data-testid="x-icon" className={className}>X</span>
80
+ ),
81
+ Check: ({ className }: { className?: string }) => (
82
+ <span data-testid="check-icon" className={className}>✓</span>
83
+ ),
84
+ };
85
+ });
86
+
87
+ interface TestData extends DataRecord {
88
+ id: string;
89
+ name: string;
90
+ email: string;
91
+ age: number;
92
+ status: string;
93
+ }
94
+
95
+ describe('[component] EditableRow', () => {
96
+ const columnHelper = createColumnHelper<TestData>();
97
+
98
+ // Helper to create columns without default cell renderers
99
+ const createColumn = <T extends DataRecord>(id: string, config: any): ColumnDef<T> => ({
100
+ id,
101
+ accessorKey: id,
102
+ ...config,
103
+ });
104
+
105
+ const defaultColumns: ColumnDef<TestData>[] = [
106
+ {
107
+ id: 'name',
108
+ accessorKey: 'name',
109
+ header: 'Name',
110
+ editable: true,
111
+ cell: undefined, // Explicitly no cell renderer to force renderEditField
112
+ },
113
+ {
114
+ id: 'email',
115
+ accessorKey: 'email',
116
+ header: 'Email',
117
+ editable: true,
118
+ cell: undefined,
119
+ },
120
+ {
121
+ id: 'age',
122
+ accessorKey: 'age',
123
+ header: 'Age',
124
+ editable: true,
125
+ fieldType: 'number' as const,
126
+ cell: undefined,
127
+ },
128
+ {
129
+ id: 'status',
130
+ accessorKey: 'status',
131
+ header: 'Status',
132
+ editable: true,
133
+ fieldType: 'select' as const,
134
+ fieldOptions: [
135
+ { value: 'active', label: 'Active' },
136
+ { value: 'inactive', label: 'Inactive' },
137
+ ],
138
+ cell: undefined,
139
+ },
140
+ ];
141
+
142
+ const defaultData: TestData = {
143
+ id: '1',
144
+ name: 'John Doe',
145
+ email: 'john@example.com',
146
+ age: 30,
147
+ status: 'active',
148
+ };
149
+
150
+ const createMockRow = (data: TestData = defaultData, columns = defaultColumns) => {
151
+ // Use renderHook to call useReactTable hook properly
152
+ // Add actions column for EditableRow (it expects this column for save/cancel buttons)
153
+ const columnsWithActions: ColumnDef<TestData>[] = [
154
+ ...columns,
155
+ {
156
+ id: 'actions',
157
+ header: 'Actions',
158
+ cell: () => null, // Actions are rendered by EditableRow itself
159
+ },
160
+ ];
161
+
162
+ const { result } = renderHook(() => {
163
+ return useReactTable({
164
+ data: [data],
165
+ columns: columnsWithActions,
166
+ getCoreRowModel: getCoreRowModel(),
167
+ });
168
+ });
169
+
170
+ return result.current.getRowModel().rows[0];
171
+ };
172
+
173
+ const getDefaultProps = () => ({
174
+ row: createMockRow(),
175
+ editingData: {},
176
+ onEditingDataChange: vi.fn(),
177
+ onSave: vi.fn(),
178
+ onCancel: vi.fn(),
179
+ actions: [],
180
+ });
181
+
182
+ const defaultProps = getDefaultProps();
183
+
184
+ beforeEach(() => {
185
+ vi.clearAllMocks();
186
+ });
187
+
188
+ afterEach(() => {
189
+ vi.clearAllMocks();
190
+ });
191
+
192
+ describe('Rendering', () => {
193
+ it('renders editable row with table row structure', () => {
194
+ render(<EditableRow {...defaultProps} />);
195
+
196
+ const row = screen.getByRole('row');
197
+ expect(row).toBeInTheDocument();
198
+ });
199
+
200
+ it('renders input fields for editable columns', () => {
201
+ const props = getDefaultProps();
202
+ // Provide editingData with the values to trigger input rendering
203
+ props.editingData = {
204
+ name: 'John Doe',
205
+ email: 'john@example.com',
206
+ };
207
+ render(<EditableRow {...props} />);
208
+
209
+ const nameInput = screen.getByDisplayValue('John Doe');
210
+ const emailInput = screen.getByDisplayValue('john@example.com');
211
+
212
+ expect(nameInput).toBeInTheDocument();
213
+ expect(emailInput).toBeInTheDocument();
214
+ });
215
+
216
+ it('renders number input for number field type', () => {
217
+ const props = getDefaultProps();
218
+ props.editingData = { age: 30 };
219
+ render(<EditableRow {...props} />);
220
+
221
+ const ageInput = screen.getByDisplayValue('30');
222
+ expect(ageInput).toHaveAttribute('type', 'number');
223
+ });
224
+
225
+ it('renders select for select field type', () => {
226
+ const props = getDefaultProps();
227
+ props.editingData = { status: 'active' };
228
+ render(<EditableRow {...props} />);
229
+
230
+ // Select is rendered as a form with data-testid="select-root" or as select-trigger
231
+ const selectTrigger = screen.getByTestId('select-trigger');
232
+ expect(selectTrigger).toBeInTheDocument();
233
+ expect(selectTrigger).toHaveAttribute('data-value', 'active');
234
+ });
235
+
236
+ it('renders save and cancel buttons', () => {
237
+ render(<EditableRow {...defaultProps} />);
238
+
239
+ expect(screen.getByRole('button', { name: /save changes/i })).toBeInTheDocument();
240
+ expect(screen.getByRole('button', { name: /cancel editing/i })).toBeInTheDocument();
241
+ });
242
+
243
+ it('auto-focuses first input field', () => {
244
+ const props = getDefaultProps();
245
+ props.editingData = { name: 'John Doe' };
246
+ render(<EditableRow {...props} />);
247
+
248
+ const nameInput = screen.getByDisplayValue('John Doe');
249
+ expect(nameInput).toHaveFocus();
250
+ });
251
+ });
252
+
253
+ describe('User Interactions', () => {
254
+ it('updates editing data when text input changes', async () => {
255
+ const user = userEvent.setup();
256
+ const onEditingDataChange = vi.fn();
257
+ const props = getDefaultProps();
258
+ props.editingData = { name: 'John Doe' };
259
+ props.onEditingDataChange = onEditingDataChange;
260
+
261
+ render(<EditableRow {...props} />);
262
+
263
+ const nameInput = screen.getByDisplayValue('John Doe');
264
+ await user.clear(nameInput);
265
+ await user.type(nameInput, 'Jane Doe');
266
+
267
+ expect(onEditingDataChange).toHaveBeenCalled();
268
+ });
269
+
270
+ it('calls onSave when save button is clicked', async () => {
271
+ const user = userEvent.setup();
272
+ const onSave = vi.fn();
273
+
274
+ render(<EditableRow {...defaultProps} onSave={onSave} />);
275
+
276
+ const saveButton = screen.getByRole('button', { name: /save changes/i });
277
+ await user.click(saveButton);
278
+
279
+ expect(onSave).toHaveBeenCalledTimes(1);
280
+ });
281
+
282
+ it('calls onCancel when cancel button is clicked', async () => {
283
+ const user = userEvent.setup();
284
+ const onCancel = vi.fn();
285
+ const props = getDefaultProps();
286
+ props.editingData = {};
287
+ props.onCancel = onCancel;
288
+
289
+ render(<EditableRow {...props} />);
290
+
291
+ const cancelButton = screen.getByRole('button', { name: /cancel editing/i });
292
+ await user.click(cancelButton);
293
+
294
+ expect(onCancel).toHaveBeenCalledTimes(1);
295
+ });
296
+
297
+ it('updates editing data when select value changes', async () => {
298
+ const user = userEvent.setup();
299
+ const onEditingDataChange = vi.fn();
300
+ const props = getDefaultProps();
301
+ props.editingData = { status: 'active' };
302
+ props.onEditingDataChange = onEditingDataChange;
303
+
304
+ render(<EditableRow {...props} />);
305
+
306
+ // Select renders with select-trigger
307
+ const selectTrigger = screen.getByTestId('select-trigger');
308
+ expect(selectTrigger).toBeInTheDocument();
309
+
310
+ // The real Select component handles value changes internally
311
+ // We verify the select is rendered correctly with the current value
312
+ expect(selectTrigger).toHaveAttribute('data-value', 'active');
313
+
314
+ // Note: Testing actual value changes would require opening the select and clicking an item
315
+ // which is more of an integration test. This test verifies the select is rendered.
316
+ });
317
+ });
318
+
319
+ describe('Field Types', () => {
320
+ it('renders date input for date field type', () => {
321
+ const dateColumn = columnHelper.accessor('createdAt', {
322
+ header: 'Created At',
323
+ editable: true,
324
+ fieldType: 'date',
325
+ });
326
+
327
+ const columns = [...defaultColumns, dateColumn];
328
+ const dateData: TestData = {
329
+ ...defaultData,
330
+ createdAt: new Date('2024-01-01'),
331
+ };
332
+ const row = createMockRow(dateData, columns);
333
+ const props = getDefaultProps();
334
+ props.row = row;
335
+ props.editingData = { createdAt: new Date('2024-01-01') };
336
+
337
+ render(<EditableRow {...props} />);
338
+
339
+ // Date input should be rendered
340
+ const inputs = screen.getAllByRole('textbox');
341
+ expect(inputs.length).toBeGreaterThan(0);
342
+ });
343
+
344
+ it('renders non-editable field as text when editable is false', () => {
345
+ const nonEditableColumn: ColumnDef<TestData> = {
346
+ id: 'id',
347
+ accessorKey: 'id',
348
+ header: 'ID',
349
+ editable: false,
350
+ };
351
+
352
+ const columns = [...defaultColumns, nonEditableColumn];
353
+ const row = createMockRow(defaultData, columns);
354
+ const props = getDefaultProps();
355
+ props.row = row;
356
+ props.editingData = {};
357
+
358
+ render(<EditableRow {...props} />);
359
+
360
+ // Should render static text, not input
361
+ expect(screen.getByText('1')).toBeInTheDocument();
362
+ });
363
+ });
364
+
365
+ describe('Editing Data', () => {
366
+ it('uses editingData values when provided', () => {
367
+ const editingData = {
368
+ name: 'Edited Name',
369
+ email: 'edited@example.com',
370
+ };
371
+
372
+ render(
373
+ <EditableRow
374
+ {...defaultProps}
375
+ editingData={editingData}
376
+ />
377
+ );
378
+
379
+ expect(screen.getByDisplayValue('Edited Name')).toBeInTheDocument();
380
+ expect(screen.getByDisplayValue('edited@example.com')).toBeInTheDocument();
381
+ });
382
+
383
+ it('falls back to original row values when editingData is empty', () => {
384
+ render(<EditableRow {...defaultProps} editingData={{}} />);
385
+
386
+ expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument();
387
+ expect(screen.getByDisplayValue('john@example.com')).toBeInTheDocument();
388
+ });
389
+ });
390
+
391
+ describe('System Columns', () => {
392
+ it('renders system columns (select, actions) without edit fields', () => {
393
+ const props = getDefaultProps();
394
+ props.editingData = {};
395
+ render(<EditableRow {...props} />);
396
+
397
+ // System columns should render their normal content
398
+ expect(screen.getByRole('button', { name: /save changes/i })).toBeInTheDocument();
399
+ expect(screen.getByRole('button', { name: /cancel editing/i })).toBeInTheDocument();
400
+ });
401
+ });
402
+
403
+ describe('Accessibility', () => {
404
+ it('provides proper ARIA attributes for row', () => {
405
+ render(<EditableRow {...defaultProps} />);
406
+
407
+ const row = screen.getByRole('row');
408
+ expect(row).toHaveAttribute('role', 'row');
409
+ });
410
+
411
+ it('provides aria-label for save button', () => {
412
+ render(<EditableRow {...defaultProps} />);
413
+
414
+ const saveButton = screen.getByRole('button', { name: /save changes/i });
415
+ expect(saveButton).toHaveAttribute('aria-label', 'Save changes');
416
+ });
417
+
418
+ it('provides aria-label for cancel button', () => {
419
+ render(<EditableRow {...defaultProps} />);
420
+
421
+ const cancelButton = screen.getByRole('button', { name: /cancel editing/i });
422
+ expect(cancelButton).toHaveAttribute('aria-label', 'Cancel editing');
423
+ });
424
+ });
425
+
426
+ describe('Edge Cases', () => {
427
+ it('handles empty editingData gracefully', () => {
428
+ render(<EditableRow {...defaultProps} editingData={{}} />);
429
+
430
+ expect(screen.getByRole('row')).toBeInTheDocument();
431
+ });
432
+
433
+ it('handles null values in editingData', () => {
434
+ const props = getDefaultProps();
435
+ props.editingData = {
436
+ name: null,
437
+ email: null,
438
+ } as any;
439
+
440
+ render(<EditableRow {...props} />);
441
+
442
+ expect(screen.getByRole('row')).toBeInTheDocument();
443
+ });
444
+
445
+ it('handles missing column definitions', () => {
446
+ const row = createMockRow();
447
+
448
+ expect(() => {
449
+ render(<EditableRow {...defaultProps} row={row} />);
450
+ }).not.toThrow();
451
+ });
452
+ });
453
+ });
454
+