@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,617 @@
1
+ /**
2
+ * @file GroupingDropdown Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for GroupingDropdown 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 { GroupingDropdown } from '../GroupingDropdown';
16
+ import type { DataTableColumn, DataRecord, SimpleColumn } from '../../types';
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
+ Group: ({ className }: { className?: string }) => (
24
+ <div data-testid="group-icon" className={className}>Group</div>
25
+ ),
26
+ };
27
+ });
28
+
29
+ // Mock Button component
30
+ vi.mock('../../Button/Button', () => ({
31
+ Button: ({ children, onClick, variant, className, ...props }: any) => (
32
+ <button
33
+ onClick={onClick}
34
+ data-variant={variant}
35
+ className={className}
36
+ {...props}
37
+ >
38
+ {children}
39
+ </button>
40
+ ),
41
+ }));
42
+
43
+ // Mock Select components - use importActual to avoid missing ChevronDown
44
+ vi.mock('../../Select/Select', async () => {
45
+ const actual = await vi.importActual('../../Select/Select');
46
+ return {
47
+ ...actual,
48
+ Select: ({ children, className }: any) => (
49
+ <div data-testid="select" className={className}>
50
+ {children}
51
+ </div>
52
+ ),
53
+ SelectTrigger: ({ children, asChild, className }: any) =>
54
+ asChild ? children : <button data-testid="select-trigger" className={className}>{children}</button>,
55
+ SelectContent: ({ children }: any) => (
56
+ <div data-testid="select-content">{children}</div>
57
+ ),
58
+ SelectItem: ({ children, onClick, value, className }: any) => (
59
+ <button
60
+ data-testid={`select-item-${value}`}
61
+ onClick={onClick}
62
+ value={value}
63
+ className={className}
64
+ >
65
+ {children}
66
+ </button>
67
+ ),
68
+ SelectValue: ({ placeholder }: any) => (
69
+ <span data-testid="select-value">{placeholder}</span>
70
+ ),
71
+ };
72
+ });
73
+
74
+ interface TestData extends DataRecord {
75
+ id: string;
76
+ name: string;
77
+ category: string;
78
+ }
79
+
80
+ describe('[component] GroupingDropdown', () => {
81
+ let handleGroupByChange: ReturnType<typeof vi.fn>;
82
+
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
85
+ handleGroupByChange = vi.fn();
86
+ });
87
+
88
+ afterEach(() => {
89
+ vi.clearAllMocks();
90
+ });
91
+
92
+ describe('Rendering', () => {
93
+ it('returns null when no groupable columns', () => {
94
+ const columns: DataTableColumn<TestData>[] = [
95
+ {
96
+ accessorKey: 'name',
97
+ header: 'Name',
98
+ enableGrouping: false,
99
+ },
100
+ ];
101
+
102
+ const { container } = render(
103
+ <GroupingDropdown
104
+ columns={columns}
105
+ currentGroupBy={null}
106
+ onGroupByChange={handleGroupByChange}
107
+ />
108
+ );
109
+
110
+ expect(container.firstChild).toBeNull();
111
+ });
112
+
113
+ it('renders dropdown when groupable columns exist', () => {
114
+ const columns: DataTableColumn<TestData>[] = [
115
+ {
116
+ accessorKey: 'category',
117
+ header: 'Category',
118
+ enableGrouping: true,
119
+ },
120
+ ];
121
+
122
+ render(
123
+ <GroupingDropdown
124
+ columns={columns}
125
+ currentGroupBy={null}
126
+ onGroupByChange={handleGroupByChange}
127
+ />
128
+ );
129
+
130
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
131
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
132
+ });
133
+
134
+ it('renders Group icon', () => {
135
+ const columns: DataTableColumn<TestData>[] = [
136
+ {
137
+ accessorKey: 'category',
138
+ header: 'Category',
139
+ enableGrouping: true,
140
+ },
141
+ ];
142
+
143
+ render(
144
+ <GroupingDropdown
145
+ columns={columns}
146
+ currentGroupBy={null}
147
+ onGroupByChange={handleGroupByChange}
148
+ />
149
+ );
150
+
151
+ expect(screen.getByTestId('group-icon')).toBeInTheDocument();
152
+ });
153
+
154
+ it('renders "Grouping" text in trigger', () => {
155
+ const columns: DataTableColumn<TestData>[] = [
156
+ {
157
+ accessorKey: 'category',
158
+ header: 'Category',
159
+ enableGrouping: true,
160
+ },
161
+ ];
162
+
163
+ render(
164
+ <GroupingDropdown
165
+ columns={columns}
166
+ currentGroupBy={null}
167
+ onGroupByChange={handleGroupByChange}
168
+ />
169
+ );
170
+
171
+ expect(screen.getByText('Grouping')).toBeInTheDocument();
172
+ });
173
+
174
+ it('renders "No grouping" option', async () => {
175
+ const user = userEvent.setup();
176
+ const columns: DataTableColumn<TestData>[] = [
177
+ {
178
+ accessorKey: 'category',
179
+ header: 'Category',
180
+ enableGrouping: true,
181
+ },
182
+ ];
183
+
184
+ render(
185
+ <GroupingDropdown
186
+ columns={columns}
187
+ currentGroupBy={null}
188
+ onGroupByChange={handleGroupByChange}
189
+ />
190
+ );
191
+
192
+ const trigger = screen.getByTestId('select-trigger');
193
+ await user.click(trigger);
194
+
195
+ await waitFor(() => {
196
+ expect(screen.getByText('No grouping')).toBeInTheDocument();
197
+ });
198
+ });
199
+
200
+ it('renders all groupable columns', async () => {
201
+ const user = userEvent.setup();
202
+ const columns: DataTableColumn<TestData>[] = [
203
+ {
204
+ accessorKey: 'category',
205
+ header: 'Category',
206
+ enableGrouping: true,
207
+ },
208
+ {
209
+ accessorKey: 'name',
210
+ header: 'Name',
211
+ enableGrouping: false,
212
+ },
213
+ {
214
+ accessorKey: 'status',
215
+ header: 'Status',
216
+ enableGrouping: true,
217
+ },
218
+ ];
219
+
220
+ render(
221
+ <GroupingDropdown
222
+ columns={columns}
223
+ currentGroupBy={null}
224
+ onGroupByChange={handleGroupByChange}
225
+ />
226
+ );
227
+
228
+ const trigger = screen.getByTestId('select-trigger');
229
+ await user.click(trigger);
230
+
231
+ await waitFor(() => {
232
+ expect(screen.getByText('Category')).toBeInTheDocument();
233
+ expect(screen.getByText('Status')).toBeInTheDocument();
234
+ expect(screen.queryByText('Name')).not.toBeInTheDocument();
235
+ });
236
+ });
237
+ });
238
+
239
+ describe('User Interactions', () => {
240
+ it('calls onGroupByChange with null when "No grouping" is clicked', async () => {
241
+ const user = userEvent.setup();
242
+ const columns: DataTableColumn<TestData>[] = [
243
+ {
244
+ accessorKey: 'category',
245
+ header: 'Category',
246
+ enableGrouping: true,
247
+ },
248
+ ];
249
+
250
+ render(
251
+ <GroupingDropdown
252
+ columns={columns}
253
+ currentGroupBy="category"
254
+ onGroupByChange={handleGroupByChange}
255
+ />
256
+ );
257
+
258
+ const trigger = screen.getByTestId('select-trigger');
259
+ await user.click(trigger);
260
+
261
+ await waitFor(() => {
262
+ const items = screen.getAllByTestId('select-item');
263
+ const noGroupingItem = items.find(item => item.getAttribute('data-value') === 'no-grouping');
264
+ expect(noGroupingItem).toBeInTheDocument();
265
+ });
266
+
267
+ const items = screen.getAllByTestId('select-item');
268
+ const noGroupingItem = items.find(item => item.getAttribute('data-value') === 'no-grouping');
269
+ await user.click(noGroupingItem);
270
+
271
+ expect(handleGroupByChange).toHaveBeenCalledTimes(1);
272
+ expect(handleGroupByChange).toHaveBeenCalledWith(null);
273
+ });
274
+
275
+ it('calls onGroupByChange with column ID when column is clicked', async () => {
276
+ const user = userEvent.setup();
277
+ const columns: DataTableColumn<TestData>[] = [
278
+ {
279
+ accessorKey: 'category',
280
+ header: 'Category',
281
+ enableGrouping: true,
282
+ },
283
+ ];
284
+
285
+ render(
286
+ <GroupingDropdown
287
+ columns={columns}
288
+ currentGroupBy={null}
289
+ onGroupByChange={handleGroupByChange}
290
+ />
291
+ );
292
+
293
+ const trigger = screen.getByTestId('select-trigger');
294
+ await user.click(trigger);
295
+
296
+ await waitFor(() => {
297
+ const items = screen.getAllByTestId('select-item');
298
+ const categoryItem = items.find(item => item.getAttribute('data-value') === 'category');
299
+ expect(categoryItem).toBeInTheDocument();
300
+ });
301
+
302
+ const items = screen.getAllByTestId('select-item');
303
+ const categoryItem = items.find(item => item.getAttribute('data-value') === 'category');
304
+ await user.click(categoryItem);
305
+
306
+ expect(handleGroupByChange).toHaveBeenCalledTimes(1);
307
+ expect(handleGroupByChange).toHaveBeenCalledWith('category');
308
+ });
309
+
310
+ it('highlights selected grouping option', async () => {
311
+ const user = userEvent.setup();
312
+ const columns: DataTableColumn<TestData>[] = [
313
+ {
314
+ accessorKey: 'category',
315
+ header: 'Category',
316
+ enableGrouping: true,
317
+ },
318
+ ];
319
+
320
+ render(
321
+ <GroupingDropdown
322
+ columns={columns}
323
+ currentGroupBy="category"
324
+ onGroupByChange={handleGroupByChange}
325
+ />
326
+ );
327
+
328
+ const trigger = screen.getByTestId('select-trigger');
329
+ await user.click(trigger);
330
+
331
+ await waitFor(() => {
332
+ const items = screen.getAllByTestId('select-item');
333
+ const categoryItem = items.find(item => item.getAttribute('data-value') === 'category');
334
+ expect(categoryItem).toBeInTheDocument();
335
+ expect(categoryItem?.className).toContain('bg-main-50');
336
+ expect(categoryItem?.className).toContain('text-main-700');
337
+ });
338
+ });
339
+
340
+ it('highlights "No grouping" when currentGroupBy is null', async () => {
341
+ const user = userEvent.setup();
342
+ const columns: DataTableColumn<TestData>[] = [
343
+ {
344
+ accessorKey: 'category',
345
+ header: 'Category',
346
+ enableGrouping: true,
347
+ },
348
+ ];
349
+
350
+ render(
351
+ <GroupingDropdown
352
+ columns={columns}
353
+ currentGroupBy={null}
354
+ onGroupByChange={handleGroupByChange}
355
+ />
356
+ );
357
+
358
+ const trigger = screen.getByTestId('select-trigger');
359
+ await user.click(trigger);
360
+
361
+ await waitFor(() => {
362
+ const items = screen.getAllByTestId('select-item');
363
+ const noGroupingItem = items.find(item => item.getAttribute('data-value') === 'no-grouping');
364
+ expect(noGroupingItem).toBeInTheDocument();
365
+ expect(noGroupingItem?.className).toContain('bg-main-50');
366
+ expect(noGroupingItem?.className).toContain('text-main-700');
367
+ });
368
+ });
369
+ });
370
+
371
+ describe('SimpleColumn Support', () => {
372
+ it('handles SimpleColumn format', () => {
373
+ const columns: SimpleColumn<TestData>[] = [
374
+ {
375
+ key: 'category',
376
+ label: 'Category',
377
+ },
378
+ ];
379
+
380
+ const { container } = render(
381
+ <GroupingDropdown
382
+ columns={columns}
383
+ currentGroupBy={null}
384
+ onGroupByChange={handleGroupByChange}
385
+ />
386
+ );
387
+
388
+ // SimpleColumn doesn't support grouping by default, so should return null
389
+ expect(container.firstChild).toBeNull();
390
+ });
391
+
392
+ it('uses SimpleColumn key as accessorKey', () => {
393
+ const columns: (DataTableColumn<TestData> | SimpleColumn<TestData>)[] = [
394
+ {
395
+ key: 'category',
396
+ label: 'Category',
397
+ } as SimpleColumn<TestData>,
398
+ {
399
+ accessorKey: 'status',
400
+ header: 'Status',
401
+ enableGrouping: true,
402
+ },
403
+ ];
404
+
405
+ render(
406
+ <GroupingDropdown
407
+ columns={columns}
408
+ currentGroupBy={null}
409
+ onGroupByChange={handleGroupByChange}
410
+ />
411
+ );
412
+
413
+ // Only the DataTableColumn with enableGrouping should be shown
414
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
415
+ });
416
+ });
417
+
418
+ describe('Column Display Names', () => {
419
+ it('uses column header as display name', async () => {
420
+ const user = userEvent.setup();
421
+ const columns: DataTableColumn<TestData>[] = [
422
+ {
423
+ accessorKey: 'category',
424
+ header: 'Product Category',
425
+ enableGrouping: true,
426
+ },
427
+ ];
428
+
429
+ render(
430
+ <GroupingDropdown
431
+ columns={columns}
432
+ currentGroupBy={null}
433
+ onGroupByChange={handleGroupByChange}
434
+ />
435
+ );
436
+
437
+ const trigger = screen.getByTestId('select-trigger');
438
+ await user.click(trigger);
439
+
440
+ await waitFor(() => {
441
+ expect(screen.getByText('Product Category')).toBeInTheDocument();
442
+ });
443
+ });
444
+
445
+ it('uses accessorKey as display name when header is not available', async () => {
446
+ const user = userEvent.setup();
447
+ const columns: DataTableColumn<TestData>[] = [
448
+ {
449
+ accessorKey: 'category',
450
+ header: undefined, // Explicitly no header
451
+ enableGrouping: true,
452
+ },
453
+ ];
454
+
455
+ render(
456
+ <GroupingDropdown
457
+ columns={columns}
458
+ currentGroupBy={null}
459
+ onGroupByChange={handleGroupByChange}
460
+ />
461
+ );
462
+
463
+ const trigger = screen.getByTestId('select-trigger');
464
+ await user.click(trigger);
465
+
466
+ await waitFor(() => {
467
+ expect(screen.getByText('category')).toBeInTheDocument();
468
+ });
469
+ });
470
+ });
471
+
472
+ describe('Edge Cases', () => {
473
+ it('handles empty columns array', () => {
474
+ const { container } = render(
475
+ <GroupingDropdown
476
+ columns={[]}
477
+ currentGroupBy={null}
478
+ onGroupByChange={handleGroupByChange}
479
+ />
480
+ );
481
+
482
+ expect(container.firstChild).toBeNull();
483
+ });
484
+
485
+ it('handles columns with undefined accessorKey', () => {
486
+ const columns: any[] = [
487
+ {
488
+ header: 'Test',
489
+ enableGrouping: true,
490
+ },
491
+ ];
492
+
493
+ const { container } = render(
494
+ <GroupingDropdown
495
+ columns={columns}
496
+ currentGroupBy={null}
497
+ onGroupByChange={handleGroupByChange}
498
+ />
499
+ );
500
+
501
+ expect(container.firstChild).toBeNull();
502
+ });
503
+
504
+ it('handles unknown column format gracefully', () => {
505
+ const columns: any[] = [
506
+ {
507
+ unknown: 'format',
508
+ },
509
+ ];
510
+
511
+ const { container } = render(
512
+ <GroupingDropdown
513
+ columns={columns}
514
+ currentGroupBy={null}
515
+ onGroupByChange={handleGroupByChange}
516
+ />
517
+ );
518
+
519
+ expect(container.firstChild).toBeNull();
520
+ });
521
+
522
+ it('handles multiple groupable columns', async () => {
523
+ const user = userEvent.setup();
524
+ const columns: DataTableColumn<TestData>[] = [
525
+ {
526
+ accessorKey: 'category',
527
+ header: 'Category',
528
+ enableGrouping: true,
529
+ },
530
+ {
531
+ accessorKey: 'status',
532
+ header: 'Status',
533
+ enableGrouping: true,
534
+ },
535
+ {
536
+ accessorKey: 'type',
537
+ header: 'Type',
538
+ enableGrouping: true,
539
+ },
540
+ ];
541
+
542
+ render(
543
+ <GroupingDropdown
544
+ columns={columns}
545
+ currentGroupBy={null}
546
+ onGroupByChange={handleGroupByChange}
547
+ />
548
+ );
549
+
550
+ const trigger = screen.getByTestId('select-trigger');
551
+ await user.click(trigger);
552
+
553
+ await waitFor(() => {
554
+ expect(screen.getByText('Category')).toBeInTheDocument();
555
+ expect(screen.getByText('Status')).toBeInTheDocument();
556
+ expect(screen.getByText('Type')).toBeInTheDocument();
557
+ });
558
+ });
559
+ });
560
+
561
+ describe('Custom Styling', () => {
562
+ it('applies custom className', () => {
563
+ const columns: DataTableColumn<TestData>[] = [
564
+ {
565
+ accessorKey: 'category',
566
+ header: 'Category',
567
+ enableGrouping: true,
568
+ },
569
+ ];
570
+
571
+ render(
572
+ <GroupingDropdown
573
+ columns={columns}
574
+ currentGroupBy={null}
575
+ onGroupByChange={handleGroupByChange}
576
+ className="custom-class"
577
+ />
578
+ );
579
+
580
+ // The className is passed to Button, which is wrapped by SelectTrigger with asChild
581
+ // When SelectTrigger uses asChild, it clones the Button element
582
+ // The Button component should receive the className prop and merge it
583
+ // Note: SelectTrigger may need to preserve the child's className when using asChild
584
+ const button = screen.getByRole('combobox');
585
+
586
+ // Check if the custom class is present in the button's className
587
+ // If SelectTrigger properly merges, it should be there
588
+ // If not, this test documents the current behavior
589
+ const classNames = button.className || '';
590
+ expect(classNames).toContain('custom-class');
591
+ });
592
+ });
593
+
594
+ describe('Accessibility', () => {
595
+ it('provides accessible button structure', () => {
596
+ const columns: DataTableColumn<TestData>[] = [
597
+ {
598
+ accessorKey: 'category',
599
+ header: 'Category',
600
+ enableGrouping: true,
601
+ },
602
+ ];
603
+
604
+ render(
605
+ <GroupingDropdown
606
+ columns={columns}
607
+ currentGroupBy={null}
608
+ onGroupByChange={handleGroupByChange}
609
+ />
610
+ );
611
+
612
+ const button = screen.getByRole('combobox');
613
+ expect(button).toBeInTheDocument();
614
+ });
615
+ });
616
+ });
617
+