@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,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file EmptyState Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for EmptyState 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 } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { EmptyState } from '../EmptyState';
|
|
16
|
+
|
|
17
|
+
// Mock lucide-react icons
|
|
18
|
+
vi.mock('lucide-react', () => ({
|
|
19
|
+
Database: ({ className }: { className?: string }) => (
|
|
20
|
+
<div data-testid="lucide-database" className={className}>Database</div>
|
|
21
|
+
),
|
|
22
|
+
Search: ({ className }: { className?: string }) => (
|
|
23
|
+
<div data-testid="lucide-search" className={className}>Search</div>
|
|
24
|
+
),
|
|
25
|
+
Plus: ({ className }: { className?: string }) => (
|
|
26
|
+
<div data-testid="lucide-plus" className={className}>Plus</div>
|
|
27
|
+
),
|
|
28
|
+
User: ({ className }: { className?: string }) => (
|
|
29
|
+
<div data-testid="lucide-user" className={className}>User</div>
|
|
30
|
+
),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Mock Button component
|
|
34
|
+
vi.mock('../../Button/Button', () => ({
|
|
35
|
+
Button: ({ children, onClick, variant, ...props }: any) => (
|
|
36
|
+
<button onClick={onClick} data-variant={variant} {...props}>
|
|
37
|
+
{children}
|
|
38
|
+
</button>
|
|
39
|
+
),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('[component] EmptyState', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Rendering', () => {
|
|
52
|
+
it('renders with default content when no props provided', () => {
|
|
53
|
+
render(<EmptyState />);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByTestId('lucide-database')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders with custom title', () => {
|
|
61
|
+
render(<EmptyState title="Custom Title" />);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('Custom Title')).toBeInTheDocument();
|
|
64
|
+
expect(screen.queryByText('No data available')).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders with custom description', () => {
|
|
68
|
+
render(<EmptyState description="Custom description" />);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('Custom description')).toBeInTheDocument();
|
|
71
|
+
expect(screen.queryByText('Get started by adding your first entry')).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders with custom icon component', () => {
|
|
75
|
+
const CustomIcon = ({ className }: { className?: string }) => (
|
|
76
|
+
<div data-testid="custom-icon" className={className}>Custom</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
render(<EmptyState icon={CustomIcon} />);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
|
82
|
+
expect(screen.queryByTestId('lucide-database')).not.toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses default Database icon when no icon provided', () => {
|
|
86
|
+
render(<EmptyState />);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByTestId('lucide-database')).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Filtered State', () => {
|
|
93
|
+
it('shows filtered state message when isFiltered is true', () => {
|
|
94
|
+
render(<EmptyState isFiltered={true} />);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('No results found')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByText('Try adjusting your search or filter criteria')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('shows default state message when isFiltered is false', () => {
|
|
101
|
+
render(<EmptyState isFiltered={false} />);
|
|
102
|
+
|
|
103
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
104
|
+
expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('shows custom title and description even when filtered', () => {
|
|
108
|
+
render(
|
|
109
|
+
<EmptyState
|
|
110
|
+
isFiltered={true}
|
|
111
|
+
title="Custom Filter Title"
|
|
112
|
+
description="Custom filter description"
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(screen.getByText('Custom Filter Title')).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByText('Custom filter description')).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Action Button', () => {
|
|
122
|
+
it('renders action button when action prop provided', () => {
|
|
123
|
+
const handleAction = vi.fn();
|
|
124
|
+
|
|
125
|
+
render(
|
|
126
|
+
<EmptyState
|
|
127
|
+
action={{
|
|
128
|
+
label: 'Create Item',
|
|
129
|
+
onClick: handleAction,
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const actionButton = screen.getByRole('button', { name: /Create Item/i });
|
|
135
|
+
expect(actionButton).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('calls action onClick when action button is clicked', async () => {
|
|
139
|
+
const user = userEvent.setup();
|
|
140
|
+
const handleAction = vi.fn();
|
|
141
|
+
|
|
142
|
+
render(
|
|
143
|
+
<EmptyState
|
|
144
|
+
action={{
|
|
145
|
+
label: 'Create Item',
|
|
146
|
+
onClick: handleAction,
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const actionButton = screen.getByRole('button', { name: /Create Item/i });
|
|
152
|
+
await user.click(actionButton);
|
|
153
|
+
|
|
154
|
+
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renders Plus icon in action button', () => {
|
|
158
|
+
const handleAction = vi.fn();
|
|
159
|
+
|
|
160
|
+
render(
|
|
161
|
+
<EmptyState
|
|
162
|
+
action={{
|
|
163
|
+
label: 'Add New',
|
|
164
|
+
onClick: handleAction,
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(screen.getByTestId('lucide-plus')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('does not render action button when action not provided', () => {
|
|
173
|
+
render(<EmptyState />);
|
|
174
|
+
|
|
175
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Clear Filters Button', () => {
|
|
180
|
+
it('renders clear filters button when isFiltered and onClearFilters provided', () => {
|
|
181
|
+
const handleClearFilters = vi.fn();
|
|
182
|
+
|
|
183
|
+
render(
|
|
184
|
+
<EmptyState
|
|
185
|
+
isFiltered={true}
|
|
186
|
+
onClearFilters={handleClearFilters}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const clearButton = screen.getByRole('button', { name: /Clear filters/i });
|
|
191
|
+
expect(clearButton).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('calls onClearFilters when clear filters button is clicked', async () => {
|
|
195
|
+
const user = userEvent.setup();
|
|
196
|
+
const handleClearFilters = vi.fn();
|
|
197
|
+
|
|
198
|
+
render(
|
|
199
|
+
<EmptyState
|
|
200
|
+
isFiltered={true}
|
|
201
|
+
onClearFilters={handleClearFilters}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const clearButton = screen.getByRole('button', { name: /Clear filters/i });
|
|
206
|
+
await user.click(clearButton);
|
|
207
|
+
|
|
208
|
+
expect(handleClearFilters).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('renders Search icon in clear filters button', () => {
|
|
212
|
+
const handleClearFilters = vi.fn();
|
|
213
|
+
|
|
214
|
+
render(
|
|
215
|
+
<EmptyState
|
|
216
|
+
isFiltered={true}
|
|
217
|
+
onClearFilters={handleClearFilters}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(screen.getByTestId('lucide-search')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('does not render clear filters button when not filtered', () => {
|
|
225
|
+
const handleClearFilters = vi.fn();
|
|
226
|
+
|
|
227
|
+
render(
|
|
228
|
+
<EmptyState
|
|
229
|
+
isFiltered={false}
|
|
230
|
+
onClearFilters={handleClearFilters}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(screen.queryByRole('button', { name: /Clear filters/i })).not.toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not render clear filters button when onClearFilters not provided', () => {
|
|
238
|
+
render(<EmptyState isFiltered={true} />);
|
|
239
|
+
|
|
240
|
+
expect(screen.queryByRole('button', { name: /Clear filters/i })).not.toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('Combined Actions', () => {
|
|
245
|
+
it('renders both action and clear filters buttons when appropriate', () => {
|
|
246
|
+
const handleAction = vi.fn();
|
|
247
|
+
const handleClearFilters = vi.fn();
|
|
248
|
+
|
|
249
|
+
render(
|
|
250
|
+
<EmptyState
|
|
251
|
+
isFiltered={true}
|
|
252
|
+
onClearFilters={handleClearFilters}
|
|
253
|
+
action={{
|
|
254
|
+
label: 'Create Item',
|
|
255
|
+
onClick: handleAction,
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(screen.getByRole('button', { name: /Clear filters/i })).toBeInTheDocument();
|
|
261
|
+
expect(screen.getByRole('button', { name: /Create Item/i })).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('calls correct handler for each button', async () => {
|
|
265
|
+
const user = userEvent.setup();
|
|
266
|
+
const handleAction = vi.fn();
|
|
267
|
+
const handleClearFilters = vi.fn();
|
|
268
|
+
|
|
269
|
+
render(
|
|
270
|
+
<EmptyState
|
|
271
|
+
isFiltered={true}
|
|
272
|
+
onClearFilters={handleClearFilters}
|
|
273
|
+
action={{
|
|
274
|
+
label: 'Create Item',
|
|
275
|
+
onClick: handleAction,
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await user.click(screen.getByRole('button', { name: /Clear filters/i }));
|
|
281
|
+
await user.click(screen.getByRole('button', { name: /Create Item/i }));
|
|
282
|
+
|
|
283
|
+
expect(handleClearFilters).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Accessibility', () => {
|
|
289
|
+
it('has proper ARIA role and live region', () => {
|
|
290
|
+
render(<EmptyState />);
|
|
291
|
+
|
|
292
|
+
const container = screen.getByRole('status');
|
|
293
|
+
expect(container).toBeInTheDocument();
|
|
294
|
+
expect(container).toHaveAttribute('aria-live', 'polite');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('marks icon as decorative with aria-hidden', () => {
|
|
298
|
+
render(<EmptyState />);
|
|
299
|
+
|
|
300
|
+
const icon = screen.getByTestId('lucide-database');
|
|
301
|
+
// Icon is wrapped in a component that may not have aria-hidden directly
|
|
302
|
+
// Check that it exists and is accessible
|
|
303
|
+
expect(icon).toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('uses semantic heading for title', () => {
|
|
307
|
+
render(<EmptyState title="Custom Title" />);
|
|
308
|
+
|
|
309
|
+
const heading = screen.getByRole('heading', { level: 3 });
|
|
310
|
+
expect(heading).toHaveTextContent('Custom Title');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('provides proper button labels', () => {
|
|
314
|
+
const handleAction = vi.fn();
|
|
315
|
+
|
|
316
|
+
render(
|
|
317
|
+
<EmptyState
|
|
318
|
+
action={{
|
|
319
|
+
label: 'Create Item',
|
|
320
|
+
onClick: handleAction,
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const button = screen.getByRole('button', { name: /Create Item/i });
|
|
326
|
+
expect(button).toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Icon Detection', () => {
|
|
331
|
+
it('identifies Database icon correctly', () => {
|
|
332
|
+
render(<EmptyState />);
|
|
333
|
+
|
|
334
|
+
const icon = screen.getByTestId('lucide-database');
|
|
335
|
+
expect(icon).toBeInTheDocument();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('identifies User icon correctly', () => {
|
|
339
|
+
const UserIcon = () => <div data-testid="lucide-user">User</div>;
|
|
340
|
+
|
|
341
|
+
render(<EmptyState icon={UserIcon} />);
|
|
342
|
+
|
|
343
|
+
const icon = screen.getByTestId('lucide-user');
|
|
344
|
+
expect(icon).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('uses custom testid for unknown icons', () => {
|
|
348
|
+
const CustomIcon = () => <div>Custom</div>;
|
|
349
|
+
|
|
350
|
+
render(<EmptyState icon={CustomIcon} />);
|
|
351
|
+
|
|
352
|
+
// Should still render but with different testid handling
|
|
353
|
+
const container = screen.getByRole('status');
|
|
354
|
+
expect(container).toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('Edge Cases', () => {
|
|
359
|
+
it('handles empty title string', () => {
|
|
360
|
+
render(<EmptyState title="" />);
|
|
361
|
+
|
|
362
|
+
// Should use default title
|
|
363
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('handles empty description string', () => {
|
|
367
|
+
render(<EmptyState description="" />);
|
|
368
|
+
|
|
369
|
+
// Should use default description
|
|
370
|
+
expect(screen.getByText('Get started by adding your first entry')).toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('handles action with empty label', () => {
|
|
374
|
+
const handleAction = vi.fn();
|
|
375
|
+
|
|
376
|
+
render(
|
|
377
|
+
<EmptyState
|
|
378
|
+
action={{
|
|
379
|
+
label: '',
|
|
380
|
+
onClick: handleAction,
|
|
381
|
+
}}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Button should still render
|
|
386
|
+
const button = screen.getByRole('button');
|
|
387
|
+
expect(button).toBeInTheDocument();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('handles rapid button clicks', async () => {
|
|
391
|
+
const user = userEvent.setup();
|
|
392
|
+
const handleAction = vi.fn();
|
|
393
|
+
|
|
394
|
+
render(
|
|
395
|
+
<EmptyState
|
|
396
|
+
action={{
|
|
397
|
+
label: 'Create Item',
|
|
398
|
+
onClick: handleAction,
|
|
399
|
+
}}
|
|
400
|
+
/>
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const button = screen.getByRole('button', { name: /Create Item/i });
|
|
404
|
+
await user.click(button);
|
|
405
|
+
await user.click(button);
|
|
406
|
+
await user.click(button);
|
|
407
|
+
|
|
408
|
+
expect(handleAction).toHaveBeenCalledTimes(3);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Layout and Styling', () => {
|
|
413
|
+
it('renders with centered flex layout', () => {
|
|
414
|
+
render(<EmptyState />);
|
|
415
|
+
|
|
416
|
+
const container = screen.getByRole('status');
|
|
417
|
+
expect(container).toHaveClass('flex', 'flex-col', 'items-center', 'justify-center');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('applies text-center class', () => {
|
|
421
|
+
render(<EmptyState />);
|
|
422
|
+
|
|
423
|
+
const container = screen.getByRole('status');
|
|
424
|
+
expect(container).toHaveClass('text-center');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('applies padding', () => {
|
|
428
|
+
render(<EmptyState />);
|
|
429
|
+
|
|
430
|
+
const container = screen.getByRole('status');
|
|
431
|
+
expect(container).toHaveClass('p-8');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file LoadingState Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Components/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for LoadingState component following testing guidelines.
|
|
8
|
+
* Tests cover all major functionality and accessibility.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render, screen } from '@testing-library/react';
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { LoadingState } from '../LoadingState';
|
|
15
|
+
|
|
16
|
+
describe('[component] LoadingState', () => {
|
|
17
|
+
describe('Rendering', () => {
|
|
18
|
+
it('renders loading spinner and text', () => {
|
|
19
|
+
render(<LoadingState />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders with centered layout', () => {
|
|
25
|
+
render(<LoadingState />);
|
|
26
|
+
|
|
27
|
+
// Find the outer container with text-center and p-8 classes
|
|
28
|
+
const container = screen.getByText('Loading...').parentElement?.parentElement;
|
|
29
|
+
expect(container).toHaveClass('text-center', 'p-8');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders with padding', () => {
|
|
33
|
+
render(<LoadingState />);
|
|
34
|
+
|
|
35
|
+
// Find the outer container with p-8 class
|
|
36
|
+
const container = screen.getByText('Loading...').parentElement?.parentElement;
|
|
37
|
+
expect(container).toHaveClass('p-8');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders spinner with animation class', () => {
|
|
41
|
+
render(<LoadingState />);
|
|
42
|
+
|
|
43
|
+
const spinner = screen.getByText('Loading...').previousElementSibling;
|
|
44
|
+
expect(spinner).toHaveClass('animate-spin');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders flex container with items centered', () => {
|
|
48
|
+
render(<LoadingState />);
|
|
49
|
+
|
|
50
|
+
const flexContainer = screen.getByText('Loading...').parentElement;
|
|
51
|
+
expect(flexContainer).toHaveClass('flex', 'items-center', 'justify-center');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Accessibility', () => {
|
|
56
|
+
it('provides aria-live region for loading state', () => {
|
|
57
|
+
render(<LoadingState />);
|
|
58
|
+
|
|
59
|
+
const loadingText = screen.getByText('Loading...');
|
|
60
|
+
expect(loadingText).toHaveAttribute('aria-live', 'polite');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('announces loading state to screen readers', () => {
|
|
64
|
+
render(<LoadingState />);
|
|
65
|
+
|
|
66
|
+
const loadingText = screen.getByText('Loading...');
|
|
67
|
+
expect(loadingText).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Visual Structure', () => {
|
|
72
|
+
it('renders spinner before loading text', () => {
|
|
73
|
+
render(<LoadingState />);
|
|
74
|
+
|
|
75
|
+
const container = screen.getByText('Loading...').parentElement;
|
|
76
|
+
const spinner = container?.firstElementChild;
|
|
77
|
+
const text = container?.lastElementChild;
|
|
78
|
+
|
|
79
|
+
expect(spinner).toBeInTheDocument();
|
|
80
|
+
expect(text).toHaveTextContent('Loading...');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('applies space between spinner and text', () => {
|
|
84
|
+
render(<LoadingState />);
|
|
85
|
+
|
|
86
|
+
const container = screen.getByText('Loading...').parentElement;
|
|
87
|
+
expect(container).toHaveClass('space-x-2');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Styling', () => {
|
|
92
|
+
it('applies muted foreground color to text', () => {
|
|
93
|
+
render(<LoadingState />);
|
|
94
|
+
|
|
95
|
+
const text = screen.getByText('Loading...');
|
|
96
|
+
expect(text).toHaveClass('text-muted-foreground');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('spinner has rounded-full class', () => {
|
|
100
|
+
render(<LoadingState />);
|
|
101
|
+
|
|
102
|
+
const spinner = screen.getByText('Loading...').previousElementSibling;
|
|
103
|
+
expect(spinner).toHaveClass('rounded-full');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('spinner has border styling', () => {
|
|
107
|
+
render(<LoadingState />);
|
|
108
|
+
|
|
109
|
+
const spinner = screen.getByText('Loading...').previousElementSibling;
|
|
110
|
+
expect(spinner).toHaveClass('border-b-2', 'border-primary');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('spinner has appropriate size', () => {
|
|
114
|
+
render(<LoadingState />);
|
|
115
|
+
|
|
116
|
+
const spinner = screen.getByText('Loading...').previousElementSibling;
|
|
117
|
+
expect(spinner).toHaveClass('h-6', 'w-6');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|