@jmruthers/pace-core 0.5.110 → 0.5.111
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-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +13 -8
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- 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/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/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 +4 -4
- 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/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- 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 +36 -36
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +3 -3
- package/docs/rbac/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +37 -13
- package/src/rbac/api.ts +25 -8
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +4 -3
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +12 -2
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +22 -7
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3J5N2T2N.js.map +0 -1
- package/dist/chunk-7GBEBJLR.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-HADXAZT3.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-XRSP3H52.js.map +0 -1
- /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ActionButtons Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for ActionButtons component following testing guidelines.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and user interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen, within } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { ActionButtons } from '../ActionButtons';
|
|
16
|
+
import type { Row } from '@tanstack/react-table';
|
|
17
|
+
import type { DataTableAction, DataRecord } from '../../types';
|
|
18
|
+
|
|
19
|
+
// Mock lucide-react icons - need to include ChevronDown for Select
|
|
20
|
+
vi.mock('lucide-react', async () => {
|
|
21
|
+
const actual = await vi.importActual('lucide-react');
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
MoreHorizontal: ({ className }: { className?: string }) => (
|
|
25
|
+
<div data-testid="more-horizontal-icon" className={className}>More</div>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Mock logger
|
|
31
|
+
vi.mock('../../../utils/logger', () => ({
|
|
32
|
+
createLogger: () => ({
|
|
33
|
+
debug: vi.fn(),
|
|
34
|
+
info: vi.fn(),
|
|
35
|
+
warn: vi.fn(),
|
|
36
|
+
error: vi.fn(),
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock Button component
|
|
41
|
+
vi.mock('../../Button/Button', () => ({
|
|
42
|
+
Button: ({ children, onClick, disabled, 'aria-label': ariaLabel, 'data-testid': testId, ...props }: any) => (
|
|
43
|
+
<button
|
|
44
|
+
onClick={onClick}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
aria-label={ariaLabel}
|
|
47
|
+
data-testid={testId}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</button>
|
|
52
|
+
),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Mock Select components - use importOriginal to avoid missing icons
|
|
56
|
+
vi.mock('../../Select/Select', async () => {
|
|
57
|
+
const actual = await vi.importActual('../../Select/Select');
|
|
58
|
+
return {
|
|
59
|
+
...actual,
|
|
60
|
+
Select: ({ children }: { children: React.ReactNode }) => <div data-testid="select">{children}</div>,
|
|
61
|
+
SelectTrigger: ({ children, asChild, className }: any) =>
|
|
62
|
+
asChild ? children : <button data-testid="select-trigger" className={className}>{children}</button>,
|
|
63
|
+
SelectContent: ({ children, className }: any) => (
|
|
64
|
+
<div data-testid="select-content" className={className}>{children}</div>
|
|
65
|
+
),
|
|
66
|
+
SelectItem: ({ children, onClick, value, 'data-testid': testId, className }: any) => (
|
|
67
|
+
<button
|
|
68
|
+
data-testid={testId}
|
|
69
|
+
onClick={onClick}
|
|
70
|
+
value={value}
|
|
71
|
+
className={className}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</button>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
interface TestData extends DataRecord {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
active: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const createMockRow = (data: TestData): Row<TestData> => ({
|
|
86
|
+
original: data,
|
|
87
|
+
id: data.id,
|
|
88
|
+
index: 0,
|
|
89
|
+
depth: 0,
|
|
90
|
+
getValue: vi.fn(),
|
|
91
|
+
renderValue: vi.fn(),
|
|
92
|
+
getUniqueValues: vi.fn(),
|
|
93
|
+
toggleSelected: vi.fn(),
|
|
94
|
+
getIsSelected: vi.fn(() => false),
|
|
95
|
+
getIsSomeSelected: vi.fn(() => false),
|
|
96
|
+
getIsAllSubRowsSelected: vi.fn(() => false),
|
|
97
|
+
getCanSelect: vi.fn(() => true),
|
|
98
|
+
getCanSelectSubRows: vi.fn(() => false),
|
|
99
|
+
getCanMultiSelect: vi.fn(() => true),
|
|
100
|
+
getToggleSelectedHandler: vi.fn(),
|
|
101
|
+
getParentRow: vi.fn(),
|
|
102
|
+
getParentRows: vi.fn(),
|
|
103
|
+
subRows: [],
|
|
104
|
+
getLeafValues: vi.fn(),
|
|
105
|
+
getAllCells: vi.fn(),
|
|
106
|
+
getLeftVisibleCells: vi.fn(),
|
|
107
|
+
getRightVisibleCells: vi.fn(),
|
|
108
|
+
getVisibleCells: vi.fn(),
|
|
109
|
+
getCenterVisibleCells: vi.fn(),
|
|
110
|
+
} as unknown as Row<TestData>);
|
|
111
|
+
|
|
112
|
+
describe('[component] ActionButtons', () => {
|
|
113
|
+
const mockRow = createMockRow({ id: '1', name: 'Test User', active: true });
|
|
114
|
+
const defaultPermissions = {
|
|
115
|
+
canRead: { can: true, isLoading: false },
|
|
116
|
+
canCreate: { can: true, isLoading: false },
|
|
117
|
+
canUpdate: { can: true, isLoading: false },
|
|
118
|
+
canDelete: { can: true, isLoading: false },
|
|
119
|
+
canExport: { can: true, isLoading: false },
|
|
120
|
+
canImport: { can: true, isLoading: false },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
vi.clearAllMocks();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
vi.clearAllMocks();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Rendering', () => {
|
|
132
|
+
it('returns null when no actions provided', () => {
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<ActionButtons row={mockRow} actions={[]} />
|
|
135
|
+
);
|
|
136
|
+
expect(container.firstChild).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('renders action buttons when actions are provided', () => {
|
|
140
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
141
|
+
{
|
|
142
|
+
label: 'Edit',
|
|
143
|
+
onClick: vi.fn(),
|
|
144
|
+
testId: 'edit-action',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: 'Delete',
|
|
148
|
+
onClick: vi.fn(),
|
|
149
|
+
testId: 'delete-action',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
render(
|
|
154
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByTestId('delete-action')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('renders action buttons with icons', () => {
|
|
162
|
+
const MockIcon = ({ className }: { className?: string }) => (
|
|
163
|
+
<span data-testid="edit-icon" className={className}>Icon</span>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
167
|
+
{
|
|
168
|
+
label: 'Edit',
|
|
169
|
+
icon: MockIcon,
|
|
170
|
+
onClick: vi.fn(),
|
|
171
|
+
testId: 'edit-action',
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
render(
|
|
176
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByTestId('edit-icon')).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns null when all actions are filtered out', () => {
|
|
184
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
185
|
+
{
|
|
186
|
+
label: 'Edit',
|
|
187
|
+
onClick: vi.fn(),
|
|
188
|
+
hidden: true,
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const { container } = render(
|
|
193
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
194
|
+
);
|
|
195
|
+
expect(container.firstChild).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('User Interactions', () => {
|
|
200
|
+
it('calls onClick handler when action button is clicked', async () => {
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
const handleClick = vi.fn();
|
|
203
|
+
|
|
204
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
205
|
+
{
|
|
206
|
+
label: 'Edit',
|
|
207
|
+
onClick: handleClick,
|
|
208
|
+
testId: 'edit-action',
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
render(
|
|
213
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const editButton = screen.getByTestId('edit-action');
|
|
217
|
+
await user.click(editButton);
|
|
218
|
+
|
|
219
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
220
|
+
expect(handleClick).toHaveBeenCalledWith(mockRow.original);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not call onClick when action is disabled', async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
const handleClick = vi.fn();
|
|
226
|
+
|
|
227
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
228
|
+
{
|
|
229
|
+
label: 'Edit',
|
|
230
|
+
onClick: handleClick,
|
|
231
|
+
disabled: true,
|
|
232
|
+
testId: 'edit-action',
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
render(
|
|
237
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const editButton = screen.getByTestId('edit-action');
|
|
241
|
+
expect(editButton).toBeDisabled();
|
|
242
|
+
|
|
243
|
+
await user.click(editButton);
|
|
244
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('handles multiple action clicks correctly', async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
const handleEdit = vi.fn();
|
|
250
|
+
const handleDelete = vi.fn();
|
|
251
|
+
|
|
252
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
253
|
+
{
|
|
254
|
+
label: 'Edit',
|
|
255
|
+
onClick: handleEdit,
|
|
256
|
+
testId: 'edit-action',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: 'Delete',
|
|
260
|
+
onClick: handleDelete,
|
|
261
|
+
testId: 'delete-action',
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
render(
|
|
266
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await user.click(screen.getByTestId('edit-action'));
|
|
270
|
+
await user.click(screen.getByTestId('delete-action'));
|
|
271
|
+
|
|
272
|
+
expect(handleEdit).toHaveBeenCalledTimes(1);
|
|
273
|
+
expect(handleDelete).toHaveBeenCalledTimes(1);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('Action Visibility', () => {
|
|
278
|
+
it('filters actions based on hidden property', () => {
|
|
279
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
280
|
+
{
|
|
281
|
+
label: 'Edit',
|
|
282
|
+
onClick: vi.fn(),
|
|
283
|
+
hidden: false,
|
|
284
|
+
testId: 'edit-action',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
label: 'Delete',
|
|
288
|
+
onClick: vi.fn(),
|
|
289
|
+
hidden: true,
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
render(
|
|
294
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
298
|
+
expect(screen.queryByText('Delete')).not.toBeInTheDocument();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('filters Edit action when user lacks update permission', () => {
|
|
302
|
+
const permissions = {
|
|
303
|
+
...defaultPermissions,
|
|
304
|
+
canUpdate: { can: false, isLoading: false },
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
308
|
+
{
|
|
309
|
+
label: 'Edit',
|
|
310
|
+
onClick: vi.fn(),
|
|
311
|
+
testId: 'edit-action',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
const { container } = render(
|
|
316
|
+
<ActionButtons
|
|
317
|
+
row={mockRow}
|
|
318
|
+
actions={mockActions}
|
|
319
|
+
permissions={permissions}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(container.firstChild).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('filters Delete action when user lacks delete permission', () => {
|
|
327
|
+
const permissions = {
|
|
328
|
+
...defaultPermissions,
|
|
329
|
+
canDelete: { can: false, isLoading: false },
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
333
|
+
{
|
|
334
|
+
label: 'Delete',
|
|
335
|
+
onClick: vi.fn(),
|
|
336
|
+
testId: 'delete-action',
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const { container } = render(
|
|
341
|
+
<ActionButtons
|
|
342
|
+
row={mockRow}
|
|
343
|
+
actions={mockActions}
|
|
344
|
+
permissions={permissions}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(container.firstChild).toBeNull();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('shows action when visible condition returns true', () => {
|
|
352
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
353
|
+
{
|
|
354
|
+
label: 'Edit',
|
|
355
|
+
onClick: vi.fn(),
|
|
356
|
+
visible: (row) => row.active === true,
|
|
357
|
+
testId: 'edit-action',
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
render(
|
|
362
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('hides action when visible condition returns false', () => {
|
|
369
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
370
|
+
{
|
|
371
|
+
label: 'Edit',
|
|
372
|
+
onClick: vi.fn(),
|
|
373
|
+
visible: (row) => row.active === false,
|
|
374
|
+
testId: 'edit-action',
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
const { container } = render(
|
|
379
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(container.firstChild).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('shows action when visible boolean is true', () => {
|
|
386
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
387
|
+
{
|
|
388
|
+
label: 'Edit',
|
|
389
|
+
onClick: vi.fn(),
|
|
390
|
+
visible: true,
|
|
391
|
+
testId: 'edit-action',
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
render(
|
|
396
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('hides action when visible boolean is false', () => {
|
|
403
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
404
|
+
{
|
|
405
|
+
label: 'Edit',
|
|
406
|
+
onClick: vi.fn(),
|
|
407
|
+
visible: false,
|
|
408
|
+
},
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
const { container } = render(
|
|
412
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(container.firstChild).toBeNull();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('Action Disabled State', () => {
|
|
420
|
+
it('disables action when disabled condition returns true', () => {
|
|
421
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
422
|
+
{
|
|
423
|
+
label: 'Edit',
|
|
424
|
+
onClick: vi.fn(),
|
|
425
|
+
disabled: (row) => !row.active,
|
|
426
|
+
testId: 'edit-action',
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const inactiveRow = createMockRow({ id: '2', name: 'Inactive User', active: false });
|
|
431
|
+
render(
|
|
432
|
+
<ActionButtons row={inactiveRow} actions={mockActions} />
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const editButton = screen.getByTestId('edit-action');
|
|
436
|
+
expect(editButton).toBeDisabled();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('enables action when disabled condition returns false', () => {
|
|
440
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
441
|
+
{
|
|
442
|
+
label: 'Edit',
|
|
443
|
+
onClick: vi.fn(),
|
|
444
|
+
disabled: (row) => !row.active,
|
|
445
|
+
testId: 'edit-action',
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
render(
|
|
450
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const editButton = screen.getByTestId('edit-action');
|
|
454
|
+
expect(editButton).not.toBeDisabled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('disables action when disabled boolean is true', () => {
|
|
458
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
459
|
+
{
|
|
460
|
+
label: 'Edit',
|
|
461
|
+
onClick: vi.fn(),
|
|
462
|
+
disabled: true,
|
|
463
|
+
testId: 'edit-action',
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
render(
|
|
468
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const editButton = screen.getByTestId('edit-action');
|
|
472
|
+
expect(editButton).toBeDisabled();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('enables action when disabled boolean is false', () => {
|
|
476
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
477
|
+
{
|
|
478
|
+
label: 'Edit',
|
|
479
|
+
onClick: vi.fn(),
|
|
480
|
+
disabled: false,
|
|
481
|
+
testId: 'edit-action',
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
render(
|
|
486
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const editButton = screen.getByTestId('edit-action');
|
|
490
|
+
expect(editButton).not.toBeDisabled();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe('Hierarchical Actions', () => {
|
|
495
|
+
it('shows parent-specific actions only for parent rows', () => {
|
|
496
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
497
|
+
{
|
|
498
|
+
label: 'Parent Action',
|
|
499
|
+
onClick: vi.fn(),
|
|
500
|
+
showForParent: true,
|
|
501
|
+
testId: 'parent-action',
|
|
502
|
+
},
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
render(
|
|
506
|
+
<ActionButtons
|
|
507
|
+
row={mockRow}
|
|
508
|
+
actions={mockActions}
|
|
509
|
+
isParent={true}
|
|
510
|
+
hierarchical={true}
|
|
511
|
+
/>
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(screen.getByTestId('parent-action')).toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('hides parent-specific actions for child rows', () => {
|
|
518
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
519
|
+
{
|
|
520
|
+
label: 'Parent Action',
|
|
521
|
+
onClick: vi.fn(),
|
|
522
|
+
showForParent: true,
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
const { container } = render(
|
|
527
|
+
<ActionButtons
|
|
528
|
+
row={mockRow}
|
|
529
|
+
actions={mockActions}
|
|
530
|
+
isParent={false}
|
|
531
|
+
hierarchical={true}
|
|
532
|
+
/>
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
expect(container.firstChild).toBeNull();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('shows child-specific actions only for child rows', () => {
|
|
539
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
540
|
+
{
|
|
541
|
+
label: 'Child Action',
|
|
542
|
+
onClick: vi.fn(),
|
|
543
|
+
showForChild: true,
|
|
544
|
+
testId: 'child-action',
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
render(
|
|
549
|
+
<ActionButtons
|
|
550
|
+
row={mockRow}
|
|
551
|
+
actions={mockActions}
|
|
552
|
+
isParent={false}
|
|
553
|
+
hierarchical={true}
|
|
554
|
+
/>
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
expect(screen.getByTestId('child-action')).toBeInTheDocument();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('hides child-specific actions for parent rows', () => {
|
|
561
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
562
|
+
{
|
|
563
|
+
label: 'Child Action',
|
|
564
|
+
onClick: vi.fn(),
|
|
565
|
+
showForChild: true,
|
|
566
|
+
},
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
const { container } = render(
|
|
570
|
+
<ActionButtons
|
|
571
|
+
row={mockRow}
|
|
572
|
+
actions={mockActions}
|
|
573
|
+
isParent={true}
|
|
574
|
+
hierarchical={true}
|
|
575
|
+
/>
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
expect(container.firstChild).toBeNull();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('uses parent icon when provided for parent rows', () => {
|
|
582
|
+
const ParentIcon = () => <span data-testid="parent-icon">Parent</span>;
|
|
583
|
+
const ChildIcon = () => <span data-testid="child-icon">Child</span>;
|
|
584
|
+
|
|
585
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
586
|
+
{
|
|
587
|
+
label: 'Action',
|
|
588
|
+
icon: ChildIcon,
|
|
589
|
+
parentIcon: ParentIcon,
|
|
590
|
+
onClick: vi.fn(),
|
|
591
|
+
showForParent: true,
|
|
592
|
+
testId: 'action',
|
|
593
|
+
},
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
render(
|
|
597
|
+
<ActionButtons
|
|
598
|
+
row={mockRow}
|
|
599
|
+
actions={mockActions}
|
|
600
|
+
isParent={true}
|
|
601
|
+
hierarchical={true}
|
|
602
|
+
/>
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
expect(screen.getByTestId('parent-icon')).toBeInTheDocument();
|
|
606
|
+
expect(screen.queryByTestId('child-icon')).not.toBeInTheDocument();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('uses parent label when provided for parent rows', () => {
|
|
610
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
611
|
+
{
|
|
612
|
+
label: 'Default Label',
|
|
613
|
+
parentLabel: 'Parent Label',
|
|
614
|
+
onClick: vi.fn(),
|
|
615
|
+
showForParent: true,
|
|
616
|
+
testId: 'action',
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
render(
|
|
621
|
+
<ActionButtons
|
|
622
|
+
row={mockRow}
|
|
623
|
+
actions={mockActions}
|
|
624
|
+
isParent={true}
|
|
625
|
+
hierarchical={true}
|
|
626
|
+
/>
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const button = screen.getByTestId('action');
|
|
630
|
+
expect(button).toHaveAttribute('aria-label', 'Parent Label');
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe('Edit Mode Behavior', () => {
|
|
635
|
+
it('shows actions with showInEditMode true when in edit mode', () => {
|
|
636
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
637
|
+
{
|
|
638
|
+
label: 'Save',
|
|
639
|
+
onClick: vi.fn(),
|
|
640
|
+
showInEditMode: true,
|
|
641
|
+
testId: 'save-action',
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
render(
|
|
646
|
+
<ActionButtons
|
|
647
|
+
row={mockRow}
|
|
648
|
+
actions={mockActions}
|
|
649
|
+
isEditing={true}
|
|
650
|
+
/>
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
expect(screen.getByTestId('save-action')).toBeInTheDocument();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('hides actions with showInEditMode false when in edit mode', () => {
|
|
657
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
658
|
+
{
|
|
659
|
+
label: 'Edit',
|
|
660
|
+
onClick: vi.fn(),
|
|
661
|
+
showInEditMode: false,
|
|
662
|
+
},
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
const { container } = render(
|
|
666
|
+
<ActionButtons
|
|
667
|
+
row={mockRow}
|
|
668
|
+
actions={mockActions}
|
|
669
|
+
isEditing={true}
|
|
670
|
+
/>
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
expect(container.firstChild).toBeNull();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('shows actions with hideInViewMode false when in view mode', () => {
|
|
677
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
678
|
+
{
|
|
679
|
+
label: 'Edit',
|
|
680
|
+
onClick: vi.fn(),
|
|
681
|
+
hideInViewMode: false,
|
|
682
|
+
testId: 'edit-action',
|
|
683
|
+
},
|
|
684
|
+
];
|
|
685
|
+
|
|
686
|
+
render(
|
|
687
|
+
<ActionButtons
|
|
688
|
+
row={mockRow}
|
|
689
|
+
actions={mockActions}
|
|
690
|
+
isEditing={false}
|
|
691
|
+
/>
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
expect(screen.getByTestId('edit-action')).toBeInTheDocument();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('hides actions with hideInViewMode true when in view mode', () => {
|
|
698
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
699
|
+
{
|
|
700
|
+
label: 'Save',
|
|
701
|
+
onClick: vi.fn(),
|
|
702
|
+
hideInViewMode: true,
|
|
703
|
+
},
|
|
704
|
+
];
|
|
705
|
+
|
|
706
|
+
const { container } = render(
|
|
707
|
+
<ActionButtons
|
|
708
|
+
row={mockRow}
|
|
709
|
+
actions={mockActions}
|
|
710
|
+
isEditing={false}
|
|
711
|
+
/>
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
expect(container.firstChild).toBeNull();
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe('Dropdown Menu for Many Actions', () => {
|
|
719
|
+
it('renders dropdown menu when more than 6 actions', () => {
|
|
720
|
+
const mockActions: DataTableAction<TestData>[] = Array.from({ length: 7 }, (_, i) => ({
|
|
721
|
+
label: `Action ${i + 1}`,
|
|
722
|
+
onClick: vi.fn(),
|
|
723
|
+
testId: `action-${i + 1}`,
|
|
724
|
+
}));
|
|
725
|
+
|
|
726
|
+
render(
|
|
727
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
// Select component renders as form with select-root test ID
|
|
731
|
+
expect(screen.getByTestId('select-root')).toBeInTheDocument();
|
|
732
|
+
expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
|
|
733
|
+
expect(screen.getByText('Open menu')).toBeInTheDocument();
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('renders individual buttons when 6 or fewer actions', () => {
|
|
737
|
+
const mockActions: DataTableAction<TestData>[] = Array.from({ length: 6 }, (_, i) => ({
|
|
738
|
+
label: `Action ${i + 1}`,
|
|
739
|
+
onClick: vi.fn(),
|
|
740
|
+
testId: `action-${i + 1}`,
|
|
741
|
+
}));
|
|
742
|
+
|
|
743
|
+
render(
|
|
744
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
expect(screen.queryByTestId('select')).not.toBeInTheDocument();
|
|
748
|
+
expect(screen.getByTestId('action-1')).toBeInTheDocument();
|
|
749
|
+
expect(screen.getByTestId('action-6')).toBeInTheDocument();
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('calls onClick when action in dropdown is clicked', async () => {
|
|
753
|
+
const user = userEvent.setup();
|
|
754
|
+
const handleClick = vi.fn();
|
|
755
|
+
|
|
756
|
+
const mockActions: DataTableAction<TestData>[] = Array.from({ length: 7 }, (_, i) => ({
|
|
757
|
+
label: `Action ${i + 1}`,
|
|
758
|
+
onClick: handleClick,
|
|
759
|
+
testId: `action-${i + 1}`,
|
|
760
|
+
}));
|
|
761
|
+
|
|
762
|
+
render(
|
|
763
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// Actions are rendered as select-item elements
|
|
767
|
+
const actionItems = screen.getAllByTestId('select-item');
|
|
768
|
+
expect(actionItems.length).toBeGreaterThan(0);
|
|
769
|
+
|
|
770
|
+
// Click the first action item
|
|
771
|
+
await user.click(actionItems[0]);
|
|
772
|
+
|
|
773
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
774
|
+
expect(handleClick).toHaveBeenCalledWith(mockRow.original);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe('Action Variants', () => {
|
|
779
|
+
it('applies destructive variant for destructive actions', () => {
|
|
780
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
781
|
+
{
|
|
782
|
+
label: 'Delete',
|
|
783
|
+
onClick: vi.fn(),
|
|
784
|
+
variant: 'destructive',
|
|
785
|
+
testId: 'delete-action',
|
|
786
|
+
},
|
|
787
|
+
];
|
|
788
|
+
|
|
789
|
+
render(
|
|
790
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const deleteButton = screen.getByTestId('delete-action');
|
|
794
|
+
expect(deleteButton).toBeInTheDocument();
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('applies default variant for non-destructive actions', () => {
|
|
798
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
799
|
+
{
|
|
800
|
+
label: 'Edit',
|
|
801
|
+
onClick: vi.fn(),
|
|
802
|
+
variant: 'default',
|
|
803
|
+
testId: 'edit-action',
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
render(
|
|
808
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
const editButton = screen.getByTestId('edit-action');
|
|
812
|
+
expect(editButton).toBeInTheDocument();
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
describe('Accessibility', () => {
|
|
817
|
+
it('provides aria-label for action buttons', () => {
|
|
818
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
819
|
+
{
|
|
820
|
+
label: 'Edit',
|
|
821
|
+
onClick: vi.fn(),
|
|
822
|
+
testId: 'edit-action',
|
|
823
|
+
},
|
|
824
|
+
];
|
|
825
|
+
|
|
826
|
+
render(
|
|
827
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const editButton = screen.getByTestId('edit-action');
|
|
831
|
+
expect(editButton).toHaveAttribute('aria-label', 'Edit');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('provides aria-disabled for disabled buttons', () => {
|
|
835
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
836
|
+
{
|
|
837
|
+
label: 'Edit',
|
|
838
|
+
onClick: vi.fn(),
|
|
839
|
+
disabled: true,
|
|
840
|
+
testId: 'edit-action',
|
|
841
|
+
},
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
render(
|
|
845
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const editButton = screen.getByTestId('edit-action');
|
|
849
|
+
expect(editButton).toHaveAttribute('aria-disabled', 'true');
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('provides screen reader text for dropdown trigger', () => {
|
|
853
|
+
const mockActions: DataTableAction<TestData>[] = Array.from({ length: 7 }, (_, i) => ({
|
|
854
|
+
label: `Action ${i + 1}`,
|
|
855
|
+
onClick: vi.fn(),
|
|
856
|
+
}));
|
|
857
|
+
|
|
858
|
+
render(
|
|
859
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
expect(screen.getByText('Open menu')).toBeInTheDocument();
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
describe('Edge Cases', () => {
|
|
867
|
+
it('handles undefined actions array gracefully', () => {
|
|
868
|
+
const { container } = render(
|
|
869
|
+
<ActionButtons row={mockRow} actions={undefined} />
|
|
870
|
+
);
|
|
871
|
+
expect(container.firstChild).toBeNull();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it('handles actions with missing onClick gracefully', () => {
|
|
875
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
876
|
+
{
|
|
877
|
+
label: 'Action',
|
|
878
|
+
onClick: undefined as any,
|
|
879
|
+
testId: 'action',
|
|
880
|
+
},
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
// Should not throw error
|
|
884
|
+
expect(() => {
|
|
885
|
+
render(<ActionButtons row={mockRow} actions={mockActions} />);
|
|
886
|
+
}).not.toThrow();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('handles rapid successive clicks', async () => {
|
|
890
|
+
const user = userEvent.setup();
|
|
891
|
+
const handleClick = vi.fn();
|
|
892
|
+
|
|
893
|
+
const mockActions: DataTableAction<TestData>[] = [
|
|
894
|
+
{
|
|
895
|
+
label: 'Edit',
|
|
896
|
+
onClick: handleClick,
|
|
897
|
+
testId: 'edit-action',
|
|
898
|
+
},
|
|
899
|
+
];
|
|
900
|
+
|
|
901
|
+
render(
|
|
902
|
+
<ActionButtons row={mockRow} actions={mockActions} />
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
const editButton = screen.getByTestId('edit-action');
|
|
906
|
+
await user.click(editButton);
|
|
907
|
+
await user.click(editButton);
|
|
908
|
+
await user.click(editButton);
|
|
909
|
+
|
|
910
|
+
expect(handleClick).toHaveBeenCalledTimes(3);
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
});
|