@jmruthers/pace-core 0.5.115 → 0.5.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +41 -14
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +1 -1
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -0,0 +1,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
|
+
|