@jmruthers/pace-core 0.5.109 → 0.5.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  3. package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
  4. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  5. package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
  6. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  7. package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
  8. package/dist/chunk-2BIDKXQU.js.map +1 -0
  9. package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
  10. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  11. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  12. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  13. package/dist/chunk-IWJYNWXN.js.map +1 -0
  14. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  15. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  16. package/dist/chunk-MW73E7SP.js.map +1 -0
  17. package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
  18. package/dist/chunk-PXXS26G5.js.map +1 -0
  19. package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
  20. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  21. package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
  22. package/dist/chunk-UGVU7L7N.js.map +1 -0
  23. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  24. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  25. package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
  26. package/dist/chunk-ZL45MG76.js.map +1 -0
  27. package/dist/components.js +10 -10
  28. package/dist/hooks.d.ts +11 -1
  29. package/dist/hooks.js +9 -7
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +13 -13
  33. package/dist/providers.d.ts +2 -2
  34. package/dist/providers.js +2 -2
  35. package/dist/rbac/index.d.ts +46 -29
  36. package/dist/rbac/index.js +9 -9
  37. package/dist/utils.js +1 -1
  38. package/docs/api/classes/ColumnFactory.md +1 -1
  39. package/docs/api/classes/ErrorBoundary.md +1 -1
  40. package/docs/api/classes/InvalidScopeError.md +4 -4
  41. package/docs/api/classes/MissingUserContextError.md +4 -4
  42. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  43. package/docs/api/classes/PermissionDeniedError.md +4 -4
  44. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  45. package/docs/api/classes/RBACAuditManager.md +8 -8
  46. package/docs/api/classes/RBACCache.md +8 -8
  47. package/docs/api/classes/RBACEngine.md +9 -8
  48. package/docs/api/classes/RBACError.md +4 -4
  49. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  50. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  51. package/docs/api/classes/StorageUtils.md +1 -1
  52. package/docs/api/enums/FileCategory.md +1 -1
  53. package/docs/api/interfaces/AggregateConfig.md +1 -1
  54. package/docs/api/interfaces/ButtonProps.md +1 -1
  55. package/docs/api/interfaces/CardProps.md +1 -1
  56. package/docs/api/interfaces/ColorPalette.md +1 -1
  57. package/docs/api/interfaces/ColorShade.md +1 -1
  58. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  59. package/docs/api/interfaces/DataRecord.md +1 -1
  60. package/docs/api/interfaces/DataTableAction.md +1 -1
  61. package/docs/api/interfaces/DataTableColumn.md +1 -1
  62. package/docs/api/interfaces/DataTableProps.md +1 -1
  63. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  67. package/docs/api/interfaces/FileMetadata.md +1 -1
  68. package/docs/api/interfaces/FileReference.md +1 -1
  69. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  70. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  71. package/docs/api/interfaces/FileUploadProps.md +1 -1
  72. package/docs/api/interfaces/FooterProps.md +1 -1
  73. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  74. package/docs/api/interfaces/InputProps.md +1 -1
  75. package/docs/api/interfaces/LabelProps.md +1 -1
  76. package/docs/api/interfaces/LoginFormProps.md +1 -1
  77. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  78. package/docs/api/interfaces/NavigationContextType.md +1 -1
  79. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  80. package/docs/api/interfaces/NavigationItem.md +1 -1
  81. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  82. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  83. package/docs/api/interfaces/Organisation.md +1 -1
  84. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  85. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  86. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  87. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  88. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  89. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  90. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  91. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  92. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  93. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  94. package/docs/api/interfaces/PaletteData.md +1 -1
  95. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  96. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  99. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  103. package/docs/api/interfaces/RBACConfig.md +19 -8
  104. package/docs/api/interfaces/RBACLogger.md +5 -5
  105. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  106. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  107. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  108. package/docs/api/interfaces/RouteConfig.md +19 -6
  109. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  110. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  111. package/docs/api/interfaces/StorageConfig.md +1 -1
  112. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  113. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  114. package/docs/api/interfaces/StorageListOptions.md +1 -1
  115. package/docs/api/interfaces/StorageListResult.md +1 -1
  116. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  117. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  118. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  119. package/docs/api/interfaces/StyleImport.md +1 -1
  120. package/docs/api/interfaces/SwitchProps.md +1 -1
  121. package/docs/api/interfaces/ToastActionElement.md +1 -1
  122. package/docs/api/interfaces/ToastProps.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  124. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  126. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  130. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  133. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  134. package/docs/api/interfaces/UserEventAccess.md +1 -1
  135. package/docs/api/interfaces/UserMenuProps.md +1 -1
  136. package/docs/api/interfaces/UserProfile.md +1 -1
  137. package/docs/api/modules.md +44 -43
  138. package/docs/api-reference/hooks.md +8 -4
  139. package/docs/architecture/rpc-function-standards.md +3 -1
  140. package/docs/best-practices/common-patterns.md +3 -3
  141. package/docs/best-practices/deployment.md +10 -4
  142. package/docs/best-practices/performance.md +11 -3
  143. package/docs/core-concepts/organisations.md +8 -8
  144. package/docs/core-concepts/permissions.md +133 -72
  145. package/docs/documentation-index.md +0 -2
  146. package/docs/migration/rbac-migration.md +65 -66
  147. package/docs/rbac/README.md +114 -38
  148. package/docs/rbac/advanced-patterns.md +15 -22
  149. package/docs/rbac/api-reference.md +63 -16
  150. package/docs/rbac/examples.md +12 -12
  151. package/docs/rbac/getting-started.md +19 -19
  152. package/docs/rbac/quick-start.md +110 -35
  153. package/docs/rbac/troubleshooting.md +127 -3
  154. package/package.json +1 -1
  155. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  156. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  157. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  158. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  159. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  160. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  161. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  162. package/src/components/FileUpload/FileUpload.tsx +2 -8
  163. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  164. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  165. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  166. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  167. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  168. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  169. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  170. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  171. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  172. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  173. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  174. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  175. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  176. package/src/hooks/index.ts +1 -1
  177. package/src/hooks/useFileDisplay.ts +51 -0
  178. package/src/hooks/usePermissionCache.test.ts +112 -68
  179. package/src/hooks/usePermissionCache.ts +55 -15
  180. package/src/rbac/README.md +81 -39
  181. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  182. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  183. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  184. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  185. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  186. package/src/rbac/adapters.tsx +4 -4
  187. package/src/rbac/api.test.ts +39 -15
  188. package/src/rbac/api.ts +27 -9
  189. package/src/rbac/audit.test.ts +2 -2
  190. package/src/rbac/audit.ts +14 -5
  191. package/src/rbac/cache.test.ts +12 -0
  192. package/src/rbac/cache.ts +29 -9
  193. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  194. package/src/rbac/components/NavigationGuard.tsx +14 -14
  195. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  196. package/src/rbac/components/PagePermissionGuard.tsx +22 -38
  197. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  198. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  199. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  200. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  201. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
  202. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  203. package/src/rbac/config.ts +2 -0
  204. package/src/rbac/docs/event-based-apps.md +6 -6
  205. package/src/rbac/engine.ts +27 -7
  206. package/src/rbac/hooks/useCan.test.ts +29 -2
  207. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  208. package/src/rbac/hooks/usePermissions.ts +47 -23
  209. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  210. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  211. package/src/rbac/hooks/useRBAC.ts +0 -55
  212. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  213. package/src/rbac/permissions.test.ts +11 -7
  214. package/src/rbac/security.test.ts +2 -2
  215. package/src/rbac/security.ts +23 -8
  216. package/src/rbac/types.test.ts +2 -2
  217. package/src/rbac/types.ts +1 -2
  218. package/src/services/EventService.ts +41 -13
  219. package/src/services/__tests__/EventService.test.ts +25 -4
  220. package/src/services/interfaces/IEventService.ts +1 -0
  221. package/src/utils/file-reference.ts +9 -0
  222. package/dist/chunk-2W4WKJVF.js.map +0 -1
  223. package/dist/chunk-3TKTL5AZ.js.map +0 -1
  224. package/dist/chunk-AUXS7XSO.js.map +0 -1
  225. package/dist/chunk-F6TSYCKP.js.map +0 -1
  226. package/dist/chunk-P72NKAT5.js.map +0 -1
  227. package/dist/chunk-Q7APDV6H.js.map +0 -1
  228. package/dist/chunk-WWNOVFDC.js.map +0 -1
  229. package/docs/rbac/breaking-changes-v3.md +0 -222
  230. package/docs/rbac/migration-guide.md +0 -260
  231. /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  232. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  233. /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
  234. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  235. /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
  236. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  237. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  238. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  239. /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
  240. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
@@ -0,0 +1,609 @@
1
+ /**
2
+ * @file ColumnFilter Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/DataTable/Components/__tests__
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive test suite for ColumnFilter component following testing guidelines.
8
+ * Tests cover all major functionality, edge cases, and user interactions.
9
+ */
10
+
11
+ import React from 'react';
12
+ import { render, screen, within, 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 { ColumnFilter } from '../ColumnFilter';
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
+ X: ({ className }: { className?: string }) => <div data-testid="x-icon" className={className}>X</div>,
24
+ Filter: ({ className }: { className?: string }) => <div data-testid="filter-icon" className={className}>Filter</div>,
25
+ };
26
+ });
27
+
28
+ // Mock Input component - use forwardRef pattern
29
+ vi.mock('../../Input/Input', () => ({
30
+ Input: React.forwardRef(({ value, onChange, placeholder, type, className, 'data-testid': testId, ...props }: any, ref: any) => (
31
+ <input
32
+ ref={ref}
33
+ value={value || ''}
34
+ onChange={onChange}
35
+ placeholder={placeholder}
36
+ type={type}
37
+ className={className}
38
+ data-testid={testId || "filter-input"}
39
+ {...props}
40
+ />
41
+ )),
42
+ }));
43
+
44
+ // Mock Select components
45
+ vi.mock('../../Select/Select', async () => {
46
+ const actual = await vi.importActual('../../Select/Select');
47
+ return {
48
+ ...actual,
49
+ Select: ({ children, value, onValueChange }: any) => (
50
+ <div data-testid="filter-select" data-value={value}>
51
+ <button onClick={() => onValueChange && onValueChange('option1')}>Change</button>
52
+ {children}
53
+ </div>
54
+ ),
55
+ SelectTrigger: ({ children, className }: any) => (
56
+ <button data-testid="select-trigger" className={className}>
57
+ {children}
58
+ </button>
59
+ ),
60
+ SelectValue: ({ placeholder }: any) => (
61
+ <span data-testid="select-value">{placeholder}</span>
62
+ ),
63
+ SelectContent: ({ children }: any) => (
64
+ <div data-testid="select-content">{children}</div>
65
+ ),
66
+ SelectItem: ({ children, value }: any) => (
67
+ <div data-testid={`select-item-${value}`}>{children}</div>
68
+ ),
69
+ };
70
+ });
71
+
72
+ // Mock Button component
73
+ vi.mock('../../Button/Button', () => ({
74
+ Button: ({ children, onClick, variant, ...props }: any) => (
75
+ <button onClick={onClick} data-variant={variant} {...props}>
76
+ {children}
77
+ </button>
78
+ ),
79
+ }));
80
+
81
+ const createMockColumn = (overrides: Partial<Column<any, unknown>> = {}): Column<any, unknown> => ({
82
+ id: 'test-column',
83
+ getFilterValue: vi.fn(() => undefined),
84
+ setFilterValue: vi.fn(),
85
+ getCanFilter: vi.fn(() => true),
86
+ ...overrides,
87
+ } as unknown as Column<any, unknown>);
88
+
89
+ describe('[component] ColumnFilter', () => {
90
+ let mockColumn: Column<any, unknown>;
91
+
92
+ beforeEach(() => {
93
+ vi.clearAllMocks();
94
+ mockColumn = createMockColumn();
95
+ });
96
+
97
+ afterEach(() => {
98
+ vi.clearAllMocks();
99
+ });
100
+
101
+ describe('Rendering', () => {
102
+ it('returns null when column cannot filter', () => {
103
+ const column = createMockColumn({
104
+ getCanFilter: vi.fn(() => false),
105
+ });
106
+
107
+ const { container } = render(
108
+ <ColumnFilter column={column} />
109
+ );
110
+
111
+ expect(container.firstChild).toBeNull();
112
+ });
113
+
114
+ it('renders text input by default', () => {
115
+ render(<ColumnFilter column={mockColumn} />);
116
+
117
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
118
+ expect(input).toBeInTheDocument();
119
+ // Text input type is default, so it may not have explicit type attribute
120
+ expect(input.tagName).toBe('INPUT');
121
+ });
122
+
123
+ it('renders with default placeholder', () => {
124
+ render(<ColumnFilter column={mockColumn} />);
125
+
126
+ const input = screen.getByPlaceholderText('Filter test-column...');
127
+ expect(input).toBeInTheDocument();
128
+ });
129
+
130
+ it('renders with custom placeholder', () => {
131
+ render(<ColumnFilter column={mockColumn} placeholder="Search..." />);
132
+
133
+ const input = screen.getByPlaceholderText('Search...');
134
+ expect(input).toBeInTheDocument();
135
+ });
136
+ });
137
+
138
+ describe('Text Filter', () => {
139
+ it('renders text input for text filter type', () => {
140
+ render(<ColumnFilter column={mockColumn} filterType="text" />);
141
+
142
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
143
+ expect(input).toBeInTheDocument();
144
+ expect(input.tagName).toBe('INPUT');
145
+ });
146
+
147
+ it('updates filter value when text input changes', async () => {
148
+ const user = userEvent.setup();
149
+ const setFilterValue = vi.fn();
150
+ const column = createMockColumn({ setFilterValue });
151
+
152
+ render(<ColumnFilter column={column} filterType="text" />);
153
+
154
+ const input = screen.getByPlaceholderText('Filter test-column...');
155
+ await user.type(input, 'test');
156
+
157
+ // user.type calls onChange for each character, but the final value should be 'test'
158
+ expect(setFilterValue).toHaveBeenCalled();
159
+ // Check that it was called with the final value
160
+ const calls = setFilterValue.mock.calls;
161
+ expect(calls[calls.length - 1][0]).toBe('t'); // Last character typed
162
+ });
163
+
164
+ it('clears filter when input is emptied', async () => {
165
+ const user = userEvent.setup();
166
+ const setFilterValue = vi.fn();
167
+ const column = createMockColumn({
168
+ setFilterValue,
169
+ getFilterValue: vi.fn(() => 'test'),
170
+ });
171
+
172
+ render(<ColumnFilter column={column} filterType="text" />);
173
+
174
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
175
+ await user.clear(input);
176
+
177
+ expect(setFilterValue).toHaveBeenCalledWith(undefined);
178
+ });
179
+ });
180
+
181
+ describe('Number Filter', () => {
182
+ it('renders number input for number filter type', () => {
183
+ render(<ColumnFilter column={mockColumn} filterType="number" />);
184
+
185
+ const input = screen.getByPlaceholderText('Filter test-column...');
186
+ expect(input).toHaveAttribute('type', 'number');
187
+ });
188
+
189
+ it('applies number-no-spinners class', () => {
190
+ render(<ColumnFilter column={mockColumn} filterType="number" />);
191
+
192
+ const input = screen.getByPlaceholderText('Filter test-column...');
193
+ expect(input).toHaveClass('datatable-number-no-spinners');
194
+ });
195
+
196
+ it('converts input value to number', async () => {
197
+ const user = userEvent.setup();
198
+ const setFilterValue = vi.fn();
199
+ const column = createMockColumn({ setFilterValue });
200
+
201
+ render(<ColumnFilter column={column} filterType="number" />);
202
+
203
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
204
+ // Clear and set value directly to test number conversion
205
+ await user.clear(input);
206
+ await user.type(input, '123');
207
+
208
+ // user.type calls onChange for each character
209
+ expect(setFilterValue).toHaveBeenCalled();
210
+ // The component should convert individual characters to numbers during typing
211
+ const calls = setFilterValue.mock.calls;
212
+ expect(calls.length).toBeGreaterThan(0);
213
+ });
214
+
215
+ it('clears filter when number input is emptied', async () => {
216
+ const user = userEvent.setup();
217
+ const setFilterValue = vi.fn();
218
+ const column = createMockColumn({
219
+ setFilterValue,
220
+ getFilterValue: vi.fn(() => 123),
221
+ });
222
+
223
+ render(<ColumnFilter column={column} filterType="number" />);
224
+
225
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
226
+ await user.clear(input);
227
+
228
+ expect(setFilterValue).toHaveBeenCalledWith(undefined);
229
+ });
230
+ });
231
+
232
+ describe('Date Filter', () => {
233
+ it('renders date input for date filter type', () => {
234
+ render(<ColumnFilter column={mockColumn} filterType="date" />);
235
+
236
+ const input = screen.getByPlaceholderText('Filter test-column...');
237
+ expect(input).toHaveAttribute('type', 'date');
238
+ });
239
+
240
+ it('updates filter value when date input changes', async () => {
241
+ const user = userEvent.setup();
242
+ const setFilterValue = vi.fn();
243
+ const column = createMockColumn({ setFilterValue });
244
+
245
+ render(<ColumnFilter column={column} filterType="date" />);
246
+
247
+ const input = screen.getByPlaceholderText('Filter test-column...');
248
+ await user.type(input, '2023-01-01');
249
+
250
+ expect(setFilterValue).toHaveBeenCalledWith('2023-01-01');
251
+ });
252
+
253
+ it('clears filter when date input is emptied', async () => {
254
+ const user = userEvent.setup();
255
+ const setFilterValue = vi.fn();
256
+ const column = createMockColumn({
257
+ setFilterValue,
258
+ getFilterValue: vi.fn(() => '2023-01-01'),
259
+ });
260
+
261
+ render(<ColumnFilter column={column} filterType="date" />);
262
+
263
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
264
+ await user.clear(input);
265
+
266
+ expect(setFilterValue).toHaveBeenCalledWith(undefined);
267
+ });
268
+ });
269
+
270
+ describe('Select Filter', () => {
271
+ const selectOptions = [
272
+ { value: 'option1', label: 'Option 1' },
273
+ { value: 'option2', label: 'Option 2' },
274
+ { value: 'option3', label: 'Option 3' },
275
+ ];
276
+
277
+ it('renders select dropdown for select filter type', () => {
278
+ render(
279
+ <ColumnFilter
280
+ column={mockColumn}
281
+ filterType="select"
282
+ options={selectOptions}
283
+ />
284
+ );
285
+
286
+ // Select component renders as form with select-root test ID
287
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
288
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
289
+ });
290
+
291
+ it('renders all option items', async () => {
292
+ const user = userEvent.setup();
293
+ render(
294
+ <ColumnFilter
295
+ column={mockColumn}
296
+ filterType="select"
297
+ options={selectOptions}
298
+ />
299
+ );
300
+
301
+ // Open the select dropdown to see options
302
+ const trigger = screen.getByTestId('select-trigger');
303
+ await user.click(trigger);
304
+
305
+ // Options are rendered as select-item elements
306
+ const selectItems = screen.getAllByTestId('select-item');
307
+ expect(selectItems.length).toBeGreaterThanOrEqual(selectOptions.length + 1); // +1 for "All" option
308
+ });
309
+
310
+ it('renders "All" option for clearing filter', async () => {
311
+ const user = userEvent.setup();
312
+ render(
313
+ <ColumnFilter
314
+ column={mockColumn}
315
+ filterType="select"
316
+ options={selectOptions}
317
+ />
318
+ );
319
+
320
+ // Open the select dropdown to see "All" option
321
+ const trigger = screen.getByTestId('select-trigger');
322
+ await user.click(trigger);
323
+
324
+ // "All" option should be present
325
+ await waitFor(() => {
326
+ expect(screen.getByText('All')).toBeInTheDocument();
327
+ });
328
+ });
329
+
330
+ it('updates filter value when select option changes', async () => {
331
+ const user = userEvent.setup();
332
+ const setFilterValue = vi.fn();
333
+ const column = createMockColumn({ setFilterValue });
334
+
335
+ render(
336
+ <ColumnFilter
337
+ column={column}
338
+ filterType="select"
339
+ options={selectOptions}
340
+ />
341
+ );
342
+
343
+ // Open the select dropdown
344
+ const trigger = screen.getByTestId('select-trigger');
345
+ await user.click(trigger);
346
+
347
+ // Wait for options to be visible and click an option
348
+ await waitFor(() => {
349
+ expect(screen.getByText('Option 1')).toBeInTheDocument();
350
+ });
351
+
352
+ const option1 = screen.getByText('Option 1');
353
+ await user.click(option1);
354
+
355
+ expect(setFilterValue).toHaveBeenCalled();
356
+ });
357
+
358
+ it('clears filter when "All" option is selected', async () => {
359
+ const user = userEvent.setup();
360
+ const setFilterValue = vi.fn();
361
+ const column = createMockColumn({
362
+ setFilterValue,
363
+ getFilterValue: vi.fn(() => 'option1'),
364
+ });
365
+
366
+ render(
367
+ <ColumnFilter
368
+ column={column}
369
+ filterType="select"
370
+ options={selectOptions}
371
+ />
372
+ );
373
+
374
+ // Open the select dropdown
375
+ const trigger = screen.getByTestId('select-trigger');
376
+ await user.click(trigger);
377
+
378
+ // Wait for "All" option to be visible and click it
379
+ await waitFor(() => {
380
+ expect(screen.getByText('All')).toBeInTheDocument();
381
+ });
382
+
383
+ const allOption = screen.getByText('All');
384
+ await user.click(allOption);
385
+
386
+ expect(setFilterValue).toHaveBeenCalledWith(undefined);
387
+ });
388
+ });
389
+
390
+ describe('Clear Filter Button', () => {
391
+ it('shows clear button when filter has value', () => {
392
+ const column = createMockColumn({
393
+ getFilterValue: vi.fn(() => 'test'),
394
+ });
395
+
396
+ render(<ColumnFilter column={column} />);
397
+
398
+ const clearButton = screen.getByRole('button');
399
+ expect(clearButton).toBeInTheDocument();
400
+ expect(screen.getByTestId('x-icon')).toBeInTheDocument();
401
+ });
402
+
403
+ it('hides clear button when filter has no value', () => {
404
+ const column = createMockColumn({
405
+ getFilterValue: vi.fn(() => undefined),
406
+ });
407
+
408
+ render(<ColumnFilter column={column} />);
409
+
410
+ expect(screen.queryByTestId('x-icon')).not.toBeInTheDocument();
411
+ });
412
+
413
+ it('hides clear button when filter value is empty string', () => {
414
+ const column = createMockColumn({
415
+ getFilterValue: vi.fn(() => ''),
416
+ });
417
+
418
+ render(<ColumnFilter column={column} />);
419
+
420
+ expect(screen.queryByTestId('x-icon')).not.toBeInTheDocument();
421
+ });
422
+
423
+ it('clears filter when clear button is clicked', async () => {
424
+ const user = userEvent.setup();
425
+ const setFilterValue = vi.fn();
426
+ const column = createMockColumn({
427
+ getFilterValue: vi.fn(() => 'test'),
428
+ setFilterValue,
429
+ });
430
+
431
+ render(<ColumnFilter column={column} />);
432
+
433
+ const clearButton = screen.getByRole('button');
434
+ await user.click(clearButton);
435
+
436
+ expect(setFilterValue).toHaveBeenCalledWith(undefined);
437
+ });
438
+ });
439
+
440
+ describe('Filter Indicator', () => {
441
+ it('shows filter indicator dot when filter has value', () => {
442
+ const column = createMockColumn({
443
+ getFilterValue: vi.fn(() => 'test'),
444
+ });
445
+
446
+ render(<ColumnFilter column={column} />);
447
+
448
+ const indicator = document.querySelector('.bg-main-500.rounded-full');
449
+ expect(indicator).toBeInTheDocument();
450
+ });
451
+
452
+ it('hides filter indicator dot when filter has no value', () => {
453
+ const column = createMockColumn({
454
+ getFilterValue: vi.fn(() => undefined),
455
+ });
456
+
457
+ render(<ColumnFilter column={column} />);
458
+
459
+ const indicator = document.querySelector('.bg-main-500.rounded-full');
460
+ expect(indicator).not.toBeInTheDocument();
461
+ });
462
+ });
463
+
464
+ describe('Edge Cases', () => {
465
+ it('handles undefined filter value gracefully', () => {
466
+ const column = createMockColumn({
467
+ getFilterValue: vi.fn(() => undefined),
468
+ });
469
+
470
+ render(<ColumnFilter column={column} />);
471
+
472
+ const input = screen.getByPlaceholderText('Filter test-column...');
473
+ expect(input).toHaveValue('');
474
+ });
475
+
476
+ it('handles null filter value gracefully', () => {
477
+ const column = createMockColumn({
478
+ getFilterValue: vi.fn(() => null),
479
+ });
480
+
481
+ render(<ColumnFilter column={column} />);
482
+
483
+ const input = screen.getByPlaceholderText('Filter test-column...');
484
+ expect(input).toHaveValue('');
485
+ });
486
+
487
+ it('handles empty string filter value', () => {
488
+ const column = createMockColumn({
489
+ getFilterValue: vi.fn(() => ''),
490
+ });
491
+
492
+ render(<ColumnFilter column={column} />);
493
+
494
+ const input = screen.getByPlaceholderText('Filter test-column...');
495
+ expect(input).toHaveValue('');
496
+ });
497
+
498
+ it('handles number zero as filter value', () => {
499
+ const column = createMockColumn({
500
+ getFilterValue: vi.fn(() => 0),
501
+ });
502
+
503
+ render(<ColumnFilter column={column} filterType="number" />);
504
+
505
+ const input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
506
+ // When filter value is 0, it may be converted to empty string if falsy check occurs
507
+ // The input should still exist and be rendered
508
+ expect(input).toBeInTheDocument();
509
+ expect(input).toHaveAttribute('type', 'number');
510
+ });
511
+
512
+ it('handles missing options array in select filter', () => {
513
+ render(
514
+ <ColumnFilter
515
+ column={mockColumn}
516
+ filterType="select"
517
+ />
518
+ );
519
+
520
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
521
+ });
522
+
523
+ it('handles empty options array in select filter', () => {
524
+ render(
525
+ <ColumnFilter
526
+ column={mockColumn}
527
+ filterType="select"
528
+ options={[]}
529
+ />
530
+ );
531
+
532
+ expect(screen.getByTestId('select-root')).toBeInTheDocument();
533
+ expect(screen.getByText('All')).toBeInTheDocument(); // "All" option should still render
534
+ });
535
+ });
536
+
537
+ describe('Accessibility', () => {
538
+ it('provides placeholder for text input', () => {
539
+ render(<ColumnFilter column={mockColumn} />);
540
+
541
+ const input = screen.getByPlaceholderText('Filter test-column...');
542
+ expect(input).toBeInTheDocument();
543
+ });
544
+
545
+ it('provides placeholder for number input', () => {
546
+ render(<ColumnFilter column={mockColumn} filterType="number" />);
547
+
548
+ const input = screen.getByPlaceholderText('Filter test-column...');
549
+ expect(input).toBeInTheDocument();
550
+ });
551
+
552
+ it('provides placeholder for date input', () => {
553
+ render(<ColumnFilter column={mockColumn} filterType="date" />);
554
+
555
+ const input = screen.getByPlaceholderText('Filter test-column...');
556
+ expect(input).toBeInTheDocument();
557
+ });
558
+
559
+ it('clear button is accessible', () => {
560
+ const column = createMockColumn({
561
+ getFilterValue: vi.fn(() => 'test'),
562
+ });
563
+
564
+ render(<ColumnFilter column={column} />);
565
+
566
+ const clearButton = screen.getByRole('button');
567
+ expect(clearButton).toBeInTheDocument();
568
+ });
569
+ });
570
+
571
+ describe('Filter Value Updates', () => {
572
+ it('maintains filter value across re-renders', () => {
573
+ const column = createMockColumn({
574
+ getFilterValue: vi.fn(() => 'persisted'),
575
+ });
576
+
577
+ const { rerender } = render(<ColumnFilter column={column} />);
578
+
579
+ let input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
580
+ expect(input.value).toBe('persisted');
581
+
582
+ rerender(<ColumnFilter column={column} />);
583
+
584
+ input = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
585
+ expect(input.value).toBe('persisted');
586
+ });
587
+
588
+ it('updates when column filter value changes externally', () => {
589
+ const column = createMockColumn({
590
+ getFilterValue: vi.fn(() => 'initial'),
591
+ });
592
+
593
+ const { rerender } = render(<ColumnFilter column={column} />);
594
+
595
+ const input1 = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
596
+ expect(input1.value).toBe('initial');
597
+
598
+ // Simulate external filter change
599
+ const updatedColumn = createMockColumn({
600
+ getFilterValue: vi.fn(() => 'updated'),
601
+ });
602
+
603
+ rerender(<ColumnFilter column={updatedColumn} />);
604
+
605
+ const input2 = screen.getByPlaceholderText('Filter test-column...') as HTMLInputElement;
606
+ expect(input2.value).toBe('updated');
607
+ });
608
+ });
609
+ });