@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.
- package/CHANGELOG.md +22 -0
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +46 -29
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +9 -8
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +19 -8
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +44 -43
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/documentation-index.md +0 -2
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/README.md +114 -38
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/api-reference.md +63 -16
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +19 -19
- package/docs/rbac/quick-start.md +110 -35
- package/docs/rbac/troubleshooting.md +127 -3
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +39 -15
- package/src/rbac/api.ts +27 -9
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +22 -38
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/config.ts +2 -0
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +27 -7
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +23 -8
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3TKTL5AZ.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-F6TSYCKP.js.map +0 -1
- package/dist/chunk-P72NKAT5.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-WWNOVFDC.js.map +0 -1
- package/docs/rbac/breaking-changes-v3.md +0 -222
- package/docs/rbac/migration-guide.md +0 -260
- /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /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
|
+
});
|