@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.
Files changed (236) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
  28. package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +43 -16
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +32 -17
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ImportModal.tsx +25 -2
  163. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  164. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  165. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  166. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  168. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  169. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  170. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  171. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  172. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  173. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  174. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  175. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  176. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  177. package/src/components/EventSelector/EventSelector.tsx +5 -25
  178. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  179. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  184. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  186. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  187. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  188. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  189. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  190. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  191. package/src/components/Select/Select.tsx +8 -0
  192. package/src/components/Toast/Toast.test.tsx +8 -7
  193. package/src/components/Toast/Toast.tsx +4 -4
  194. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  195. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  196. package/src/hooks/useEventTheme.ts +49 -18
  197. package/src/hooks/usePermissionCache.ts +5 -3
  198. package/src/hooks/useSecureDataAccess.ts +11 -1
  199. package/src/hooks/useToast.ts +11 -12
  200. package/src/providers/services/EventServiceProvider.tsx +15 -8
  201. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  202. package/src/rbac/audit.test.ts +206 -0
  203. package/src/rbac/audit.ts +37 -2
  204. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  205. package/src/rbac/errors.test.ts +340 -0
  206. package/src/rbac/hooks/index.ts +9 -0
  207. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  208. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  209. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  210. package/src/services/AuthService.ts +10 -0
  211. package/src/services/EventService.ts +111 -50
  212. package/src/services/__tests__/AuthService.test.ts +1 -1
  213. package/src/services/__tests__/EventService.test.ts +60 -45
  214. package/src/services/interfaces/IEventService.ts +1 -1
  215. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  216. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  217. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  218. package/src/utils/file-reference.test.ts +214 -0
  219. package/dist/chunk-3OGQLOJM.js.map +0 -1
  220. package/dist/chunk-4OX5PXHX.js.map +0 -1
  221. package/dist/chunk-5CDJCTOO.js +0 -190
  222. package/dist/chunk-5YIZFEUQ.js.map +0 -1
  223. package/dist/chunk-F6QB26OS.js.map +0 -1
  224. package/dist/chunk-KTHLNIMA.js.map +0 -1
  225. package/dist/chunk-OO3V7W4H.js.map +0 -1
  226. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  227. package/src/rbac/audit-enhanced.ts +0 -351
  228. /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  230. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  231. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  232. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  233. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  234. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  235. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  236. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -0,0 +1,708 @@
1
+ /**
2
+ * @file ColumnVisibilityDropdown Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for ColumnVisibilityDropdown 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 { ColumnVisibilityDropdown } from '../ColumnVisibilityDropdown';
16
+ import type { Column } from '@tanstack/react-table';
17
+
18
+ // Mock lucide-react icons - use importActual to include ChevronDown for Select
19
+ vi.mock('lucide-react', async () => {
20
+ const actual = await vi.importActual('lucide-react');
21
+ return {
22
+ ...actual,
23
+ Settings2: ({ className }: { className?: string }) => (
24
+ <div data-testid="settings-2-icon" className={className}>Settings</div>
25
+ ),
26
+ Eye: ({ className }: { className?: string }) => (
27
+ <div data-testid="eye-icon" className={className}>Eye</div>
28
+ ),
29
+ EyeOff: ({ className }: { className?: string }) => (
30
+ <div data-testid="eye-off-icon" className={className}>EyeOff</div>
31
+ ),
32
+ };
33
+ });
34
+
35
+ // Mock Button component
36
+ vi.mock('../../Button/Button', () => ({
37
+ Button: ({ children, onClick, variant, size, className, ...props }: any) => (
38
+ <button
39
+ onClick={onClick}
40
+ data-variant={variant}
41
+ data-size={size}
42
+ className={className}
43
+ {...props}
44
+ >
45
+ {children}
46
+ </button>
47
+ ),
48
+ }));
49
+
50
+ // Mock Checkbox component
51
+ vi.mock('../../Checkbox/Checkbox', () => ({
52
+ Checkbox: ({ id, checked, onCheckedChange, ...props }: any) => (
53
+ <input
54
+ type="checkbox"
55
+ id={id}
56
+ checked={checked}
57
+ onChange={(e) => onCheckedChange && onCheckedChange(e.target.checked)}
58
+ {...props}
59
+ />
60
+ ),
61
+ }));
62
+
63
+ // Mock Select components - use importActual to avoid missing ChevronDown
64
+ vi.mock('../../Select/Select', async () => {
65
+ const actual = await vi.importActual('../../Select/Select');
66
+ return {
67
+ ...actual,
68
+ Select: ({ children, className }: any) => (
69
+ <form data-testid="select-root" className={className}>
70
+ {children}
71
+ </form>
72
+ ),
73
+ SelectTrigger: ({ children, asChild, className }: any) =>
74
+ asChild ? children : <button data-testid="select-trigger" role="combobox" className={className}>{children}</button>,
75
+ SelectContent: ({ children }: any) => (
76
+ <ul data-testid="select-content" role="listbox">{children}</ul>
77
+ ),
78
+ SelectItem: ({ children, onClick, value, className, ...props }: any) => (
79
+ <li
80
+ data-testid="select-item"
81
+ data-value={value}
82
+ onClick={onClick}
83
+ role="option"
84
+ className={className}
85
+ {...props}
86
+ >
87
+ {children}
88
+ </li>
89
+ ),
90
+ SelectSeparator: () => <div data-testid="select-separator" />,
91
+ };
92
+ });
93
+
94
+ const createMockColumn = (overrides: Partial<Column<any, unknown>> = {}): Column<any, unknown> => ({
95
+ id: 'test-column',
96
+ getCanHide: vi.fn(() => true),
97
+ getIsVisible: vi.fn(() => true),
98
+ columnDef: {
99
+ header: 'Test Column',
100
+ },
101
+ ...overrides,
102
+ } as unknown as Column<any, unknown>);
103
+
104
+ describe('[component] ColumnVisibilityDropdown', () => {
105
+ let mockColumns: Column<any, unknown>[];
106
+ let handleVisibilityChange: ReturnType<typeof vi.fn>;
107
+
108
+ beforeEach(() => {
109
+ vi.clearAllMocks();
110
+ handleVisibilityChange = vi.fn();
111
+
112
+ mockColumns = [
113
+ createMockColumn({ id: 'col1', columnDef: { header: 'Column 1' } }),
114
+ createMockColumn({ id: 'col2', columnDef: { header: 'Column 2' } }),
115
+ createMockColumn({ id: 'col3', columnDef: { header: 'Column 3' } }),
116
+ ];
117
+ });
118
+
119
+ afterEach(() => {
120
+ vi.clearAllMocks();
121
+ });
122
+
123
+ describe('Rendering', () => {
124
+ it('renders dropdown with trigger button', () => {
125
+ render(
126
+ <ColumnVisibilityDropdown
127
+ columns={mockColumns}
128
+ onColumnVisibilityChange={handleVisibilityChange}
129
+ />
130
+ );
131
+
132
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
133
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders Settings2 icon', () => {
137
+ render(
138
+ <ColumnVisibilityDropdown
139
+ columns={mockColumns}
140
+ onColumnVisibilityChange={handleVisibilityChange}
141
+ />
142
+ );
143
+
144
+ expect(screen.getByTestId('settings-2-icon')).toBeInTheDocument();
145
+ });
146
+
147
+ it('renders "Columns" text in trigger', () => {
148
+ render(
149
+ <ColumnVisibilityDropdown
150
+ columns={mockColumns}
151
+ onColumnVisibilityChange={handleVisibilityChange}
152
+ />
153
+ );
154
+
155
+ expect(screen.getByText('Columns')).toBeInTheDocument();
156
+ });
157
+
158
+ it('renders Show All and Hide All buttons', async () => {
159
+ const user = userEvent.setup();
160
+ render(
161
+ <ColumnVisibilityDropdown
162
+ columns={mockColumns}
163
+ onColumnVisibilityChange={handleVisibilityChange}
164
+ />
165
+ );
166
+
167
+ const trigger = screen.getByTestId('select-trigger');
168
+ await user.click(trigger);
169
+
170
+ await waitFor(() => {
171
+ expect(screen.getByText('Show All')).toBeInTheDocument();
172
+ expect(screen.getByText('Hide All')).toBeInTheDocument();
173
+ });
174
+ });
175
+
176
+ it('renders Eye icon in Show All button', async () => {
177
+ const user = userEvent.setup();
178
+ render(
179
+ <ColumnVisibilityDropdown
180
+ columns={mockColumns}
181
+ onColumnVisibilityChange={handleVisibilityChange}
182
+ />
183
+ );
184
+
185
+ const trigger = screen.getByTestId('select-trigger');
186
+ await user.click(trigger);
187
+
188
+ await waitFor(() => {
189
+ expect(screen.getByTestId('eye-icon')).toBeInTheDocument();
190
+ });
191
+ });
192
+
193
+ it('renders EyeOff icon in Hide All button', async () => {
194
+ const user = userEvent.setup();
195
+ render(
196
+ <ColumnVisibilityDropdown
197
+ columns={mockColumns}
198
+ onColumnVisibilityChange={handleVisibilityChange}
199
+ />
200
+ );
201
+
202
+ const trigger = screen.getByTestId('select-trigger');
203
+ await user.click(trigger);
204
+
205
+ await waitFor(() => {
206
+ expect(screen.getByTestId('eye-off-icon')).toBeInTheDocument();
207
+ });
208
+ });
209
+
210
+ it('renders separator between buttons and columns', async () => {
211
+ const user = userEvent.setup();
212
+ render(
213
+ <ColumnVisibilityDropdown
214
+ columns={mockColumns}
215
+ onColumnVisibilityChange={handleVisibilityChange}
216
+ />
217
+ );
218
+
219
+ const trigger = screen.getByTestId('select-trigger');
220
+ await user.click(trigger);
221
+
222
+ await waitFor(() => {
223
+ expect(screen.getByTestId('select-separator')).toBeInTheDocument();
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('Column List', () => {
229
+ it('renders all toggleable columns', async () => {
230
+ const user = userEvent.setup();
231
+ render(
232
+ <ColumnVisibilityDropdown
233
+ columns={mockColumns}
234
+ onColumnVisibilityChange={handleVisibilityChange}
235
+ />
236
+ );
237
+
238
+ const trigger = screen.getByTestId('select-trigger');
239
+ await user.click(trigger);
240
+
241
+ await waitFor(() => {
242
+ expect(screen.getByText('Column 1')).toBeInTheDocument();
243
+ expect(screen.getByText('Column 2')).toBeInTheDocument();
244
+ expect(screen.getByText('Column 3')).toBeInTheDocument();
245
+ });
246
+ });
247
+
248
+ it('filters out non-hideable columns', async () => {
249
+ const user = userEvent.setup();
250
+ const columnsWithNonHideable = [
251
+ ...mockColumns,
252
+ createMockColumn({
253
+ id: 'actions',
254
+ getCanHide: vi.fn(() => false),
255
+ columnDef: { header: 'Actions' },
256
+ }),
257
+ ];
258
+
259
+ render(
260
+ <ColumnVisibilityDropdown
261
+ columns={columnsWithNonHideable}
262
+ onColumnVisibilityChange={handleVisibilityChange}
263
+ />
264
+ );
265
+
266
+ const trigger = screen.getByTestId('select-trigger');
267
+ await user.click(trigger);
268
+
269
+ await waitFor(() => {
270
+ expect(screen.queryByText('Actions')).not.toBeInTheDocument();
271
+ });
272
+ });
273
+
274
+ it('filters out actions column', async () => {
275
+ const user = userEvent.setup();
276
+ const columnsWithActions = [
277
+ ...mockColumns,
278
+ createMockColumn({
279
+ id: 'actions',
280
+ getCanHide: vi.fn(() => true),
281
+ columnDef: { header: 'Actions' },
282
+ }),
283
+ ];
284
+
285
+ render(
286
+ <ColumnVisibilityDropdown
287
+ columns={columnsWithActions}
288
+ onColumnVisibilityChange={handleVisibilityChange}
289
+ />
290
+ );
291
+
292
+ const trigger = screen.getByTestId('select-trigger');
293
+ await user.click(trigger);
294
+
295
+ await waitFor(() => {
296
+ expect(screen.queryByText('Actions')).not.toBeInTheDocument();
297
+ });
298
+ });
299
+
300
+ it('uses column header as label when available', async () => {
301
+ const user = userEvent.setup();
302
+ render(
303
+ <ColumnVisibilityDropdown
304
+ columns={mockColumns}
305
+ onColumnVisibilityChange={handleVisibilityChange}
306
+ />
307
+ );
308
+
309
+ const trigger = screen.getByTestId('select-trigger');
310
+ await user.click(trigger);
311
+
312
+ await waitFor(() => {
313
+ expect(screen.getByText('Column 1')).toBeInTheDocument();
314
+ });
315
+ });
316
+
317
+ it('uses column id as label when header is not a string', async () => {
318
+ const user = userEvent.setup();
319
+ const columnsWithComplexHeader = [
320
+ createMockColumn({
321
+ id: 'col-complex',
322
+ columnDef: { header: () => <span>Complex Header</span> },
323
+ }),
324
+ ];
325
+
326
+ render(
327
+ <ColumnVisibilityDropdown
328
+ columns={columnsWithComplexHeader}
329
+ onColumnVisibilityChange={handleVisibilityChange}
330
+ />
331
+ );
332
+
333
+ const trigger = screen.getByTestId('select-trigger');
334
+ await user.click(trigger);
335
+
336
+ await waitFor(() => {
337
+ expect(screen.getByText('col-complex')).toBeInTheDocument();
338
+ });
339
+ });
340
+ });
341
+
342
+ describe('Checkbox States', () => {
343
+ it('renders checked checkbox for visible columns', async () => {
344
+ const user = userEvent.setup();
345
+ const visibleColumn = createMockColumn({
346
+ id: 'visible-col',
347
+ getIsVisible: vi.fn(() => true),
348
+ columnDef: { header: 'Visible Column' },
349
+ });
350
+
351
+ render(
352
+ <ColumnVisibilityDropdown
353
+ columns={[visibleColumn]}
354
+ onColumnVisibilityChange={handleVisibilityChange}
355
+ />
356
+ );
357
+
358
+ const trigger = screen.getByTestId('select-trigger');
359
+ await user.click(trigger);
360
+
361
+ await waitFor(() => {
362
+ const checkbox = screen.getByRole('checkbox', { name: /Visible Column/i });
363
+ expect(checkbox).toBeChecked();
364
+ });
365
+ });
366
+
367
+ it('renders unchecked checkbox for hidden columns', async () => {
368
+ const user = userEvent.setup();
369
+ const hiddenColumn = createMockColumn({
370
+ id: 'hidden-col',
371
+ getIsVisible: vi.fn(() => false),
372
+ columnDef: { header: 'Hidden Column' },
373
+ });
374
+
375
+ render(
376
+ <ColumnVisibilityDropdown
377
+ columns={[hiddenColumn]}
378
+ onColumnVisibilityChange={handleVisibilityChange}
379
+ />
380
+ );
381
+
382
+ const trigger = screen.getByTestId('select-trigger');
383
+ await user.click(trigger);
384
+
385
+ await waitFor(() => {
386
+ const checkbox = screen.getByRole('checkbox', { name: /Hidden Column/i });
387
+ expect(checkbox).not.toBeChecked();
388
+ });
389
+ });
390
+ });
391
+
392
+ describe('User Interactions', () => {
393
+ it('calls onColumnVisibilityChange when checkbox is toggled', async () => {
394
+ const user = userEvent.setup();
395
+ const column = createMockColumn({
396
+ id: 'toggle-col',
397
+ getIsVisible: vi.fn(() => true),
398
+ columnDef: { header: 'Toggle Column' },
399
+ });
400
+
401
+ render(
402
+ <ColumnVisibilityDropdown
403
+ columns={[column]}
404
+ onColumnVisibilityChange={handleVisibilityChange}
405
+ />
406
+ );
407
+
408
+ const trigger = screen.getByTestId('select-trigger');
409
+ await user.click(trigger);
410
+
411
+ await waitFor(() => {
412
+ const checkbox = screen.getByRole('checkbox', { name: /Toggle Column/i });
413
+ expect(checkbox).toBeInTheDocument();
414
+ });
415
+
416
+ const checkbox = screen.getByRole('checkbox', { name: /Toggle Column/i });
417
+ await user.click(checkbox);
418
+
419
+ expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
420
+ expect(handleVisibilityChange).toHaveBeenCalledWith('toggle-col', false);
421
+ });
422
+
423
+ it('calls onColumnVisibilityChange with true when hidden column is checked', async () => {
424
+ const user = userEvent.setup();
425
+ const column = createMockColumn({
426
+ id: 'hidden-col',
427
+ getIsVisible: vi.fn(() => false),
428
+ columnDef: { header: 'Hidden Column' },
429
+ });
430
+
431
+ render(
432
+ <ColumnVisibilityDropdown
433
+ columns={[column]}
434
+ onColumnVisibilityChange={handleVisibilityChange}
435
+ />
436
+ );
437
+
438
+ const trigger = screen.getByTestId('select-trigger');
439
+ await user.click(trigger);
440
+
441
+ await waitFor(() => {
442
+ const checkbox = screen.getByRole('checkbox', { name: /Hidden Column/i });
443
+ expect(checkbox).toBeInTheDocument();
444
+ });
445
+
446
+ const checkbox = screen.getByRole('checkbox', { name: /Hidden Column/i });
447
+ await user.click(checkbox);
448
+
449
+ expect(handleVisibilityChange).toHaveBeenCalledWith('hidden-col', true);
450
+ });
451
+
452
+ it('calls onColumnVisibilityChange for all hidden columns when Show All is clicked', async () => {
453
+ const user = userEvent.setup();
454
+ const columns = [
455
+ createMockColumn({
456
+ id: 'col1',
457
+ getIsVisible: vi.fn(() => false),
458
+ columnDef: { header: 'Column 1' },
459
+ }),
460
+ createMockColumn({
461
+ id: 'col2',
462
+ getIsVisible: vi.fn(() => false),
463
+ columnDef: { header: 'Column 2' },
464
+ }),
465
+ ];
466
+
467
+ render(
468
+ <ColumnVisibilityDropdown
469
+ columns={columns}
470
+ onColumnVisibilityChange={handleVisibilityChange}
471
+ />
472
+ );
473
+
474
+ const trigger = screen.getByTestId('select-trigger');
475
+ await user.click(trigger);
476
+
477
+ await waitFor(() => {
478
+ const showAllButton = screen.getByText('Show All');
479
+ expect(showAllButton).toBeInTheDocument();
480
+ });
481
+
482
+ const showAllButton = screen.getByText('Show All');
483
+ await user.click(showAllButton);
484
+
485
+ expect(handleVisibilityChange).toHaveBeenCalledTimes(2);
486
+ expect(handleVisibilityChange).toHaveBeenCalledWith('col1', true);
487
+ expect(handleVisibilityChange).toHaveBeenCalledWith('col2', true);
488
+ });
489
+
490
+ it('calls onColumnVisibilityChange for all visible columns when Hide All is clicked', async () => {
491
+ const user = userEvent.setup();
492
+ const columns = [
493
+ createMockColumn({
494
+ id: 'col1',
495
+ getIsVisible: vi.fn(() => true),
496
+ columnDef: { header: 'Column 1' },
497
+ }),
498
+ createMockColumn({
499
+ id: 'col2',
500
+ getIsVisible: vi.fn(() => true),
501
+ columnDef: { header: 'Column 2' },
502
+ }),
503
+ ];
504
+
505
+ render(
506
+ <ColumnVisibilityDropdown
507
+ columns={columns}
508
+ onColumnVisibilityChange={handleVisibilityChange}
509
+ />
510
+ );
511
+
512
+ const trigger = screen.getByTestId('select-trigger');
513
+ await user.click(trigger);
514
+
515
+ await waitFor(() => {
516
+ const hideAllButton = screen.getByText('Hide All');
517
+ expect(hideAllButton).toBeInTheDocument();
518
+ });
519
+
520
+ const hideAllButton = screen.getByText('Hide All');
521
+ await user.click(hideAllButton);
522
+
523
+ expect(handleVisibilityChange).toHaveBeenCalledTimes(2);
524
+ expect(handleVisibilityChange).toHaveBeenCalledWith('col1', false);
525
+ expect(handleVisibilityChange).toHaveBeenCalledWith('col2', false);
526
+ });
527
+
528
+ it('only shows visible columns when Show All is clicked', async () => {
529
+ const user = userEvent.setup();
530
+ const columns = [
531
+ createMockColumn({
532
+ id: 'visible-col',
533
+ getIsVisible: vi.fn(() => true),
534
+ columnDef: { header: 'Visible Column' },
535
+ }),
536
+ createMockColumn({
537
+ id: 'hidden-col',
538
+ getIsVisible: vi.fn(() => false),
539
+ columnDef: { header: 'Hidden Column' },
540
+ }),
541
+ ];
542
+
543
+ render(
544
+ <ColumnVisibilityDropdown
545
+ columns={columns}
546
+ onColumnVisibilityChange={handleVisibilityChange}
547
+ />
548
+ );
549
+
550
+ const trigger = screen.getByTestId('select-trigger');
551
+ await user.click(trigger);
552
+
553
+ await waitFor(() => {
554
+ const showAllButton = screen.getByText('Show All');
555
+ expect(showAllButton).toBeInTheDocument();
556
+ });
557
+
558
+ const showAllButton = screen.getByText('Show All');
559
+ await user.click(showAllButton);
560
+
561
+ // Only hidden column should be shown
562
+ expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
563
+ expect(handleVisibilityChange).toHaveBeenCalledWith('hidden-col', true);
564
+ });
565
+
566
+ it('only hides visible columns when Hide All is clicked', async () => {
567
+ const user = userEvent.setup();
568
+ const columns = [
569
+ createMockColumn({
570
+ id: 'visible-col',
571
+ getIsVisible: vi.fn(() => true),
572
+ columnDef: { header: 'Visible Column' },
573
+ }),
574
+ createMockColumn({
575
+ id: 'hidden-col',
576
+ getIsVisible: vi.fn(() => false),
577
+ columnDef: { header: 'Hidden Column' },
578
+ }),
579
+ ];
580
+
581
+ render(
582
+ <ColumnVisibilityDropdown
583
+ columns={columns}
584
+ onColumnVisibilityChange={handleVisibilityChange}
585
+ />
586
+ );
587
+
588
+ const trigger = screen.getByTestId('select-trigger');
589
+ await user.click(trigger);
590
+
591
+ await waitFor(() => {
592
+ const hideAllButton = screen.getByText('Hide All');
593
+ expect(hideAllButton).toBeInTheDocument();
594
+ });
595
+
596
+ const hideAllButton = screen.getByText('Hide All');
597
+ await user.click(hideAllButton);
598
+
599
+ // Only visible column should be hidden
600
+ expect(handleVisibilityChange).toHaveBeenCalledTimes(1);
601
+ expect(handleVisibilityChange).toHaveBeenCalledWith('visible-col', false);
602
+ });
603
+ });
604
+
605
+ describe('Edge Cases', () => {
606
+ it('handles empty columns array', () => {
607
+ render(
608
+ <ColumnVisibilityDropdown
609
+ columns={[]}
610
+ onColumnVisibilityChange={handleVisibilityChange}
611
+ />
612
+ );
613
+
614
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
615
+ });
616
+
617
+ it('handles columns with no toggleable columns', async () => {
618
+ const user = userEvent.setup();
619
+ const nonToggleableColumns = [
620
+ createMockColumn({
621
+ id: 'col1',
622
+ getCanHide: vi.fn(() => false),
623
+ columnDef: { header: 'Column 1' },
624
+ }),
625
+ ];
626
+
627
+ render(
628
+ <ColumnVisibilityDropdown
629
+ columns={nonToggleableColumns}
630
+ onColumnVisibilityChange={handleVisibilityChange}
631
+ />
632
+ );
633
+
634
+ const trigger = screen.getByTestId('select-trigger');
635
+ await user.click(trigger);
636
+
637
+ await waitFor(() => {
638
+ expect(screen.queryByText('Column 1')).not.toBeInTheDocument();
639
+ });
640
+ });
641
+
642
+ it('handles columns with undefined header', async () => {
643
+ const user = userEvent.setup();
644
+ const columnWithUndefinedHeader = createMockColumn({
645
+ id: 'col-undefined',
646
+ columnDef: { header: undefined },
647
+ });
648
+
649
+ render(
650
+ <ColumnVisibilityDropdown
651
+ columns={[columnWithUndefinedHeader]}
652
+ onColumnVisibilityChange={handleVisibilityChange}
653
+ />
654
+ );
655
+
656
+ const trigger = screen.getByTestId('select-trigger');
657
+ await user.click(trigger);
658
+
659
+ await waitFor(() => {
660
+ expect(screen.getByText('col-undefined')).toBeInTheDocument();
661
+ });
662
+ });
663
+ });
664
+
665
+ describe('Accessibility', () => {
666
+ it('provides checkbox labels for screen readers', async () => {
667
+ const user = userEvent.setup();
668
+ render(
669
+ <ColumnVisibilityDropdown
670
+ columns={mockColumns}
671
+ onColumnVisibilityChange={handleVisibilityChange}
672
+ />
673
+ );
674
+
675
+ const trigger = screen.getByTestId('select-trigger');
676
+ await user.click(trigger);
677
+
678
+ await waitFor(() => {
679
+ const checkboxes = screen.getAllByRole('checkbox');
680
+ expect(checkboxes.length).toBeGreaterThan(0);
681
+ });
682
+ });
683
+
684
+ it('associates labels with checkboxes', async () => {
685
+ const user = userEvent.setup();
686
+ const column = createMockColumn({
687
+ id: 'test-col',
688
+ columnDef: { header: 'Test Column' },
689
+ });
690
+
691
+ render(
692
+ <ColumnVisibilityDropdown
693
+ columns={[column]}
694
+ onColumnVisibilityChange={handleVisibilityChange}
695
+ />
696
+ );
697
+
698
+ const trigger = screen.getByTestId('select-trigger');
699
+ await user.click(trigger);
700
+
701
+ await waitFor(() => {
702
+ const checkbox = screen.getByRole('checkbox', { name: /Test Column/i });
703
+ expect(checkbox).toBeInTheDocument();
704
+ });
705
+ });
706
+ });
707
+ });
708
+