@jmruthers/pace-core 0.5.114 → 0.5.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-3JRLZXER.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-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
- 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-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
- package/dist/{chunk-JHWQNJP3.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-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
- 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 +43 -16
- 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 +32 -17
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ImportModal.tsx +25 -2
- 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.test.tsx +8 -7
- package/src/components/Toast/Toast.tsx +4 -4
- 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 +11 -12
- 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-4OX5PXHX.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-5YIZFEUQ.js.map +0 -1
- 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-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-3JRLZXER.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,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file BulkOperationsDropdown Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for BulkOperationsDropdown component following testing guidelines.
|
|
8
|
+
* Tests cover all major functionality, edge cases, and user interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { BulkOperationsDropdown } from '../BulkOperationsDropdown';
|
|
16
|
+
|
|
17
|
+
// Mock lucide-react icons - use importActual to include ChevronDown for Select
|
|
18
|
+
vi.mock('lucide-react', async () => {
|
|
19
|
+
const actual = await vi.importActual('lucide-react');
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
MoreHorizontal: ({ className }: { className?: string }) => (
|
|
23
|
+
<div data-testid="more-horizontal-icon" className={className}>More</div>
|
|
24
|
+
),
|
|
25
|
+
Download: ({ className }: { className?: string }) => (
|
|
26
|
+
<div data-testid="download-icon" className={className}>Download</div>
|
|
27
|
+
),
|
|
28
|
+
Trash: ({ className }: { className?: string }) => (
|
|
29
|
+
<div data-testid="trash-icon" className={className}>Trash</div>
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Mock Button component
|
|
35
|
+
vi.mock('../../Button/Button', () => ({
|
|
36
|
+
Button: ({ children, onClick, disabled, variant, size, className, ...props }: any) => (
|
|
37
|
+
<button
|
|
38
|
+
onClick={onClick}
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
data-variant={variant}
|
|
41
|
+
data-size={size}
|
|
42
|
+
className={className}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</button>
|
|
47
|
+
),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Mock Select components - use importActual to avoid missing ChevronDown
|
|
51
|
+
vi.mock('../../Select/Select', async () => {
|
|
52
|
+
const actual = await vi.importActual('../../Select/Select');
|
|
53
|
+
return {
|
|
54
|
+
...actual,
|
|
55
|
+
Select: ({ children, disabled }: any) => {
|
|
56
|
+
// Store disabled state in context or pass to children
|
|
57
|
+
return (
|
|
58
|
+
<form data-testid="select-root" data-disabled={disabled}>
|
|
59
|
+
{children}
|
|
60
|
+
</form>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
SelectTrigger: ({ children, asChild, className, disabled }: any) =>
|
|
64
|
+
asChild ? children : <button data-testid="select-trigger" role="combobox" disabled={disabled} className={className}>{children}</button>,
|
|
65
|
+
SelectContent: ({ children, className }: any) => (
|
|
66
|
+
<ul data-testid="select-content" role="listbox" className={className}>{children}</ul>
|
|
67
|
+
),
|
|
68
|
+
SelectItem: ({ children, onClick, value, disabled, className }: any) => (
|
|
69
|
+
<li
|
|
70
|
+
data-testid="select-item"
|
|
71
|
+
data-value={value}
|
|
72
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
73
|
+
onClick={onClick}
|
|
74
|
+
role="option"
|
|
75
|
+
aria-disabled={disabled}
|
|
76
|
+
className={className}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</li>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('[component] BulkOperationsDropdown', () => {
|
|
85
|
+
const mockSelectedRows = {
|
|
86
|
+
'row-1': true,
|
|
87
|
+
'row-2': true,
|
|
88
|
+
'row-3': false,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.clearAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.clearAllMocks();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Rendering', () => {
|
|
100
|
+
it('renders dropdown with operations', () => {
|
|
101
|
+
render(
|
|
102
|
+
<BulkOperationsDropdown
|
|
103
|
+
operations={['export', 'delete']}
|
|
104
|
+
selectedRows={mockSelectedRows}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(screen.getByTestId('select-root')).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText(/Bulk Actions/i)).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('displays selected count in button text', () => {
|
|
113
|
+
render(
|
|
114
|
+
<BulkOperationsDropdown
|
|
115
|
+
operations={['export', 'delete']}
|
|
116
|
+
selectedRows={mockSelectedRows}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(screen.getByText(/Bulk Actions \(2\)/i)).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders MoreHorizontal icon', () => {
|
|
124
|
+
render(
|
|
125
|
+
<BulkOperationsDropdown
|
|
126
|
+
operations={['export', 'delete']}
|
|
127
|
+
selectedRows={mockSelectedRows}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(screen.getByTestId('more-horizontal-icon')).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('renders all operation items', async () => {
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
render(
|
|
137
|
+
<BulkOperationsDropdown
|
|
138
|
+
operations={['export', 'delete']}
|
|
139
|
+
selectedRows={mockSelectedRows}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Open the select dropdown
|
|
144
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
145
|
+
await user.click(trigger);
|
|
146
|
+
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
const items = screen.getAllByTestId('select-item');
|
|
149
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
150
|
+
const deleteItem = items.find(item => item.getAttribute('data-value') === 'delete');
|
|
151
|
+
expect(exportItem).toBeInTheDocument();
|
|
152
|
+
expect(deleteItem).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders export operation with Download icon', async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
render(
|
|
159
|
+
<BulkOperationsDropdown
|
|
160
|
+
operations={['export']}
|
|
161
|
+
selectedRows={mockSelectedRows}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
166
|
+
await user.click(trigger);
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
const items = screen.getAllByTestId('select-item');
|
|
170
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
171
|
+
expect(exportItem).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(screen.getByTestId('download-icon')).toBeInTheDocument();
|
|
175
|
+
expect(screen.getByText('Export Selected')).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('renders delete operation with Trash icon', async () => {
|
|
179
|
+
const user = userEvent.setup();
|
|
180
|
+
render(
|
|
181
|
+
<BulkOperationsDropdown
|
|
182
|
+
operations={['delete']}
|
|
183
|
+
selectedRows={mockSelectedRows}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
188
|
+
await user.click(trigger);
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
const items = screen.getAllByTestId('select-item');
|
|
192
|
+
const deleteItem = items.find(item => item.getAttribute('data-value') === 'delete');
|
|
193
|
+
expect(deleteItem).toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(screen.getByTestId('trash-icon')).toBeInTheDocument();
|
|
197
|
+
expect(screen.getByText('Delete')).toBeInTheDocument();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Disabled State', () => {
|
|
202
|
+
it('disables dropdown when no rows are selected', () => {
|
|
203
|
+
render(
|
|
204
|
+
<BulkOperationsDropdown
|
|
205
|
+
operations={['export', 'delete']}
|
|
206
|
+
selectedRows={{}}
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// The disabled state is on the Button (trigger), not the select-root
|
|
211
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
212
|
+
expect(trigger).toBeDisabled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('disables dropdown when selectedRows is empty object', () => {
|
|
216
|
+
render(
|
|
217
|
+
<BulkOperationsDropdown
|
|
218
|
+
operations={['export', 'delete']}
|
|
219
|
+
selectedRows={{}}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// The disabled state is on the Button (trigger), not the select-root
|
|
224
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
225
|
+
expect(trigger).toBeDisabled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('disables dropdown when all rows are false', () => {
|
|
229
|
+
render(
|
|
230
|
+
<BulkOperationsDropdown
|
|
231
|
+
operations={['export', 'delete']}
|
|
232
|
+
selectedRows={{ 'row-1': false, 'row-2': false }}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// The disabled state is on the Button (trigger), not the select-root
|
|
237
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
238
|
+
expect(trigger).toBeDisabled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('enables dropdown when rows are selected', () => {
|
|
242
|
+
render(
|
|
243
|
+
<BulkOperationsDropdown
|
|
244
|
+
operations={['export', 'delete']}
|
|
245
|
+
selectedRows={mockSelectedRows}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// The disabled state is on the Button (trigger), not the select-root
|
|
250
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
251
|
+
expect(trigger).not.toBeDisabled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('disables operation items when no selection', async () => {
|
|
255
|
+
const user = userEvent.setup();
|
|
256
|
+
render(
|
|
257
|
+
<BulkOperationsDropdown
|
|
258
|
+
operations={['export', 'delete']}
|
|
259
|
+
selectedRows={{}}
|
|
260
|
+
/>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
264
|
+
await user.click(trigger);
|
|
265
|
+
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
const items = screen.getAllByTestId('select-item');
|
|
268
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
269
|
+
expect(exportItem).toBeInTheDocument();
|
|
270
|
+
expect(exportItem).toHaveAttribute('data-disabled', 'true');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('User Interactions', () => {
|
|
276
|
+
it('calls onOperation when export is clicked', async () => {
|
|
277
|
+
const user = userEvent.setup();
|
|
278
|
+
const handleOperation = vi.fn();
|
|
279
|
+
|
|
280
|
+
render(
|
|
281
|
+
<BulkOperationsDropdown
|
|
282
|
+
operations={['export']}
|
|
283
|
+
selectedRows={mockSelectedRows}
|
|
284
|
+
onOperation={handleOperation}
|
|
285
|
+
/>
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
289
|
+
await user.click(trigger);
|
|
290
|
+
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
const items = screen.getAllByTestId('select-item');
|
|
293
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
294
|
+
expect(exportItem).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const items = screen.getAllByTestId('select-item');
|
|
298
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
299
|
+
await user.click(exportItem!);
|
|
300
|
+
|
|
301
|
+
expect(handleOperation).toHaveBeenCalledTimes(1);
|
|
302
|
+
expect(handleOperation).toHaveBeenCalledWith('export', ['row-1', 'row-2']);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('calls onOperation when delete is clicked', async () => {
|
|
306
|
+
const user = userEvent.setup();
|
|
307
|
+
const handleOperation = vi.fn();
|
|
308
|
+
|
|
309
|
+
render(
|
|
310
|
+
<BulkOperationsDropdown
|
|
311
|
+
operations={['delete']}
|
|
312
|
+
selectedRows={mockSelectedRows}
|
|
313
|
+
onOperation={handleOperation}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
318
|
+
await user.click(trigger);
|
|
319
|
+
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
const items = screen.getAllByTestId('select-item');
|
|
322
|
+
const deleteItem = items.find(item => item.getAttribute('data-value') === 'delete');
|
|
323
|
+
expect(deleteItem).toBeInTheDocument();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const items = screen.getAllByTestId('select-item');
|
|
327
|
+
const deleteItem = items.find(item => item.getAttribute('data-value') === 'delete');
|
|
328
|
+
await user.click(deleteItem!);
|
|
329
|
+
|
|
330
|
+
expect(handleOperation).toHaveBeenCalledTimes(1);
|
|
331
|
+
expect(handleOperation).toHaveBeenCalledWith('delete', ['row-1', 'row-2']);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('does not call onOperation when no rows are selected', async () => {
|
|
335
|
+
const user = userEvent.setup();
|
|
336
|
+
const handleOperation = vi.fn();
|
|
337
|
+
|
|
338
|
+
render(
|
|
339
|
+
<BulkOperationsDropdown
|
|
340
|
+
operations={['export']}
|
|
341
|
+
selectedRows={{}}
|
|
342
|
+
onOperation={handleOperation}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
347
|
+
await user.click(trigger);
|
|
348
|
+
|
|
349
|
+
await waitFor(() => {
|
|
350
|
+
const items = screen.getAllByTestId('select-item');
|
|
351
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
352
|
+
expect(exportItem).toBeInTheDocument();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const items = screen.getAllByTestId('select-item');
|
|
356
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
357
|
+
if (exportItem) {
|
|
358
|
+
await user.click(exportItem);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
expect(handleOperation).not.toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('passes correct selected row IDs to onOperation', async () => {
|
|
365
|
+
const user = userEvent.setup();
|
|
366
|
+
const handleOperation = vi.fn();
|
|
367
|
+
const selectedRows = {
|
|
368
|
+
'row-1': true,
|
|
369
|
+
'row-2': false,
|
|
370
|
+
'row-3': true,
|
|
371
|
+
'row-4': false,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
render(
|
|
375
|
+
<BulkOperationsDropdown
|
|
376
|
+
operations={['export']}
|
|
377
|
+
selectedRows={selectedRows}
|
|
378
|
+
onOperation={handleOperation}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
383
|
+
await user.click(trigger);
|
|
384
|
+
|
|
385
|
+
await waitFor(() => {
|
|
386
|
+
const items = screen.getAllByTestId('select-item');
|
|
387
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
388
|
+
expect(exportItem).toBeInTheDocument();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const items = screen.getAllByTestId('select-item');
|
|
392
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
393
|
+
await user.click(exportItem!);
|
|
394
|
+
|
|
395
|
+
expect(handleOperation).toHaveBeenCalledWith('export', ['row-1', 'row-3']);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('Selected Count Display', () => {
|
|
400
|
+
it('displays correct count for multiple selections', () => {
|
|
401
|
+
render(
|
|
402
|
+
<BulkOperationsDropdown
|
|
403
|
+
operations={['export']}
|
|
404
|
+
selectedRows={mockSelectedRows}
|
|
405
|
+
/>
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(screen.getByText(/Bulk Actions \(2\)/i)).toBeInTheDocument();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('displays count of 0 when no selections', () => {
|
|
412
|
+
render(
|
|
413
|
+
<BulkOperationsDropdown
|
|
414
|
+
operations={['export']}
|
|
415
|
+
selectedRows={{}}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
expect(screen.getByText(/Bulk Actions \(0\)/i)).toBeInTheDocument();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('displays count of 1 for single selection', () => {
|
|
423
|
+
render(
|
|
424
|
+
<BulkOperationsDropdown
|
|
425
|
+
operations={['export']}
|
|
426
|
+
selectedRows={{ 'row-1': true }}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(screen.getByText(/Bulk Actions \(1\)/i)).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('Operation Variants', () => {
|
|
435
|
+
it('applies destructive variant to delete operation', async () => {
|
|
436
|
+
const user = userEvent.setup();
|
|
437
|
+
render(
|
|
438
|
+
<BulkOperationsDropdown
|
|
439
|
+
operations={['delete']}
|
|
440
|
+
selectedRows={mockSelectedRows}
|
|
441
|
+
/>
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
445
|
+
await user.click(trigger);
|
|
446
|
+
|
|
447
|
+
await waitFor(() => {
|
|
448
|
+
const items = screen.getAllByTestId('select-item');
|
|
449
|
+
const deleteItem = items.find(item => item.getAttribute('data-value') === 'delete');
|
|
450
|
+
expect(deleteItem).toBeInTheDocument();
|
|
451
|
+
expect(deleteItem?.className).toContain('text-destructive');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('applies outline variant to export operation', async () => {
|
|
456
|
+
const user = userEvent.setup();
|
|
457
|
+
render(
|
|
458
|
+
<BulkOperationsDropdown
|
|
459
|
+
operations={['export']}
|
|
460
|
+
selectedRows={mockSelectedRows}
|
|
461
|
+
/>
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const trigger = screen.getByTestId('select-trigger');
|
|
465
|
+
await user.click(trigger);
|
|
466
|
+
|
|
467
|
+
await waitFor(() => {
|
|
468
|
+
const items = screen.getAllByTestId('select-item');
|
|
469
|
+
const exportItem = items.find(item => item.getAttribute('data-value') === 'export');
|
|
470
|
+
expect(exportItem).toBeInTheDocument();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe('Edge Cases', () => {
|
|
476
|
+
it('handles undefined selectedRows gracefully', () => {
|
|
477
|
+
render(
|
|
478
|
+
<BulkOperationsDropdown
|
|
479
|
+
operations={['export']}
|
|
480
|
+
selectedRows={undefined as any}
|
|
481
|
+
/>
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
expect(screen.getByText(/Bulk Actions \(0\)/i)).toBeInTheDocument();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('handles null selectedRows gracefully', () => {
|
|
488
|
+
render(
|
|
489
|
+
<BulkOperationsDropdown
|
|
490
|
+
operations={['export']}
|
|
491
|
+
selectedRows={null as any}
|
|
492
|
+
/>
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(screen.getByText(/Bulk Actions \(0\)/i)).toBeInTheDocument();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('handles empty operations array', () => {
|
|
499
|
+
render(
|
|
500
|
+
<BulkOperationsDropdown
|
|
501
|
+
operations={[]}
|
|
502
|
+
selectedRows={mockSelectedRows}
|
|
503
|
+
/>
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
expect(screen.getByTestId('select-root')).toBeInTheDocument();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('handles single operation', () => {
|
|
510
|
+
render(
|
|
511
|
+
<BulkOperationsDropdown
|
|
512
|
+
operations={['export']}
|
|
513
|
+
selectedRows={mockSelectedRows}
|
|
514
|
+
/>
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(screen.getByTestId('select-root')).toBeInTheDocument();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('handles multiple operations', () => {
|
|
521
|
+
render(
|
|
522
|
+
<BulkOperationsDropdown
|
|
523
|
+
operations={['export', 'delete']}
|
|
524
|
+
selectedRows={mockSelectedRows}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
expect(screen.getByTestId('select-root')).toBeInTheDocument();
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe('Accessibility', () => {
|
|
533
|
+
it('provides accessible button structure', () => {
|
|
534
|
+
render(
|
|
535
|
+
<BulkOperationsDropdown
|
|
536
|
+
operations={['export']}
|
|
537
|
+
selectedRows={mockSelectedRows}
|
|
538
|
+
/>
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const button = screen.getByRole('combobox');
|
|
542
|
+
expect(button).toBeInTheDocument();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('displays selected count for screen readers', () => {
|
|
546
|
+
render(
|
|
547
|
+
<BulkOperationsDropdown
|
|
548
|
+
operations={['export']}
|
|
549
|
+
selectedRows={mockSelectedRows}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
expect(screen.getByText(/Bulk Actions \(2\)/i)).toBeInTheDocument();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('Custom Styling', () => {
|
|
558
|
+
it('applies custom className', () => {
|
|
559
|
+
const { container } = render(
|
|
560
|
+
<BulkOperationsDropdown
|
|
561
|
+
operations={['export']}
|
|
562
|
+
selectedRows={mockSelectedRows}
|
|
563
|
+
className="custom-class"
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const button = screen.getByRole('combobox');
|
|
568
|
+
expect(button.className).toContain('custom-class');
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|