@jmruthers/pace-core 0.5.1 → 0.5.4
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/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -55
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- 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 +96 -0
- 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 +235 -0
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.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 +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- 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 +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- 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/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
- 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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +952 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +364 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +551 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -55
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Header Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/Header
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive test suite for the Header component covering all functionality,
|
|
8
|
+
* edge cases, and user interactions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { User } from '@supabase/supabase-js';
|
|
16
|
+
import { Header, HeaderProps } from './Header';
|
|
17
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
18
|
+
import type { NavigationItem } from '../NavigationMenu';
|
|
19
|
+
|
|
20
|
+
// Mock the child components
|
|
21
|
+
vi.mock('../NavigationMenu', () => ({
|
|
22
|
+
NavigationMenu: ({ items, onNavigate, currentPath, buttonText, className }: any) => (
|
|
23
|
+
<div data-testid="navigation-menu" className={className}>
|
|
24
|
+
<button data-testid="navigation-menu-trigger">{buttonText}</button>
|
|
25
|
+
{items?.map((item: any) => (
|
|
26
|
+
<div key={item.id} data-testid={`nav-item-${item.id}`}>
|
|
27
|
+
{item.label}
|
|
28
|
+
</div>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('../UserMenu', () => ({
|
|
35
|
+
UserMenu: ({ user, onSignOut, onChangePassword, className }: any) => (
|
|
36
|
+
<div data-testid="user-menu" className={className}>
|
|
37
|
+
<button data-testid="user-menu-trigger">
|
|
38
|
+
{user ? user.email : 'Sign In'}
|
|
39
|
+
</button>
|
|
40
|
+
{user && (
|
|
41
|
+
<div data-testid="user-actions">
|
|
42
|
+
<button data-testid="sign-out-btn" onClick={onSignOut}>
|
|
43
|
+
Sign Out
|
|
44
|
+
</button>
|
|
45
|
+
<button data-testid="change-password-btn" onClick={() => onChangePassword?.('new', 'new')}>
|
|
46
|
+
Change Password
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock('../EventSelector', () => ({
|
|
55
|
+
EventSelector: ({ placeholder, className, 'data-testid': testId }: any) => (
|
|
56
|
+
<div data-testid={testId || 'event-selector'} className={className}>
|
|
57
|
+
<button>{placeholder}</button>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Test data
|
|
63
|
+
const mockUser: User = {
|
|
64
|
+
id: '123',
|
|
65
|
+
email: 'test@example.com',
|
|
66
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
67
|
+
updated_at: '2023-01-01T00:00:00Z',
|
|
68
|
+
aud: 'authenticated',
|
|
69
|
+
role: 'authenticated',
|
|
70
|
+
app_metadata: {},
|
|
71
|
+
user_metadata: {},
|
|
72
|
+
} as User;
|
|
73
|
+
|
|
74
|
+
const mockNavItems: NavigationItem[] = [
|
|
75
|
+
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
|
|
76
|
+
{ id: 'users', label: 'Users', href: '/users' },
|
|
77
|
+
{ id: 'settings', label: 'Settings', href: '/settings' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const mockProps: HeaderProps = {
|
|
81
|
+
logoUrl: '/test-logo.svg',
|
|
82
|
+
logoAlt: 'Test Logo',
|
|
83
|
+
navItems: mockNavItems,
|
|
84
|
+
user: mockUser,
|
|
85
|
+
onSignOut: vi.fn(),
|
|
86
|
+
onChangePassword: vi.fn(),
|
|
87
|
+
currentPath: '/dashboard',
|
|
88
|
+
onNavigate: vi.fn(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe('Header Component', () => {
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
vi.clearAllMocks();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Basic rendering tests
|
|
101
|
+
describe('Rendering', () => {
|
|
102
|
+
it('renders with default props', () => {
|
|
103
|
+
renderWithProviders(<Header />);
|
|
104
|
+
|
|
105
|
+
const header = screen.getByRole('banner');
|
|
106
|
+
expect(header).toBeInTheDocument();
|
|
107
|
+
expect(header).toHaveClass('w-full', 'border-b', 'border-main-200', 'h-16');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('renders with custom className', () => {
|
|
111
|
+
renderWithProviders(<Header className="custom-header" />);
|
|
112
|
+
|
|
113
|
+
const header = screen.getByRole('banner');
|
|
114
|
+
expect(header).toHaveClass('custom-header');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('renders with proper semantic structure', () => {
|
|
118
|
+
renderWithProviders(<Header />);
|
|
119
|
+
|
|
120
|
+
const header = screen.getByRole('banner');
|
|
121
|
+
const nav = screen.getByRole('navigation');
|
|
122
|
+
|
|
123
|
+
expect(header).toBeInTheDocument();
|
|
124
|
+
expect(nav).toBeInTheDocument();
|
|
125
|
+
expect(nav).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Logo rendering tests
|
|
130
|
+
describe('Logo Rendering', () => {
|
|
131
|
+
it('renders logo from URL when provided', () => {
|
|
132
|
+
renderWithProviders(<Header logoUrl="/test-logo.svg" logoAlt="Test Logo" />);
|
|
133
|
+
|
|
134
|
+
const logo = screen.getByRole('img', { name: 'Test Logo' });
|
|
135
|
+
expect(logo).toBeInTheDocument();
|
|
136
|
+
expect(logo).toHaveAttribute('src', '/test-logo.svg');
|
|
137
|
+
expect(logo).toHaveAttribute('alt', 'Test Logo');
|
|
138
|
+
expect(logo).toHaveClass('h-[2.15rem]', 'w-auto', 'max-w-[200px]');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('renders custom logo component when provided', () => {
|
|
142
|
+
const CustomLogo = () => <div data-testid="custom-logo">Custom Logo</div>;
|
|
143
|
+
|
|
144
|
+
renderWithProviders(<Header logo={<CustomLogo />} />);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByTestId('custom-logo')).toBeInTheDocument();
|
|
147
|
+
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders default logo when no logo props provided', () => {
|
|
151
|
+
renderWithProviders(<Header />);
|
|
152
|
+
|
|
153
|
+
const logo = screen.getByRole('img', { name: 'Logo' });
|
|
154
|
+
expect(logo).toBeInTheDocument();
|
|
155
|
+
expect(logo).toHaveAttribute('src', expect.stringContaining('data:image/svg+xml'));
|
|
156
|
+
expect(logo).toHaveClass('h-8', 'w-8');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('uses logoAlt as fallback when logoUrl provided without logoAlt', () => {
|
|
160
|
+
renderWithProviders(<Header logoUrl="/test-logo.svg" />);
|
|
161
|
+
|
|
162
|
+
const logo = screen.getByRole('img', { name: 'Logo' });
|
|
163
|
+
expect(logo).toHaveAttribute('alt', 'Logo');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('prioritizes custom logo over logoUrl', () => {
|
|
167
|
+
const CustomLogo = () => <div data-testid="custom-logo">Custom Logo</div>;
|
|
168
|
+
|
|
169
|
+
renderWithProviders(
|
|
170
|
+
<Header
|
|
171
|
+
logo={<CustomLogo />}
|
|
172
|
+
logoUrl="/test-logo.svg"
|
|
173
|
+
logoAlt="Test Logo"
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(screen.getByTestId('custom-logo')).toBeInTheDocument();
|
|
178
|
+
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Navigation menu tests
|
|
183
|
+
describe('Navigation Menu', () => {
|
|
184
|
+
it('renders navigation menu when navItems provided', () => {
|
|
185
|
+
renderWithProviders(<Header navItems={mockNavItems} />);
|
|
186
|
+
|
|
187
|
+
expect(screen.getByTestId('navigation-menu')).toBeInTheDocument();
|
|
188
|
+
expect(screen.getByTestId('navigation-menu-trigger')).toHaveTextContent('Menu');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('does not render navigation menu when navItems empty', () => {
|
|
192
|
+
renderWithProviders(<Header navItems={[]} />);
|
|
193
|
+
|
|
194
|
+
expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('does not render navigation menu when navItems undefined', () => {
|
|
198
|
+
renderWithProviders(<Header />);
|
|
199
|
+
|
|
200
|
+
expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('passes correct props to NavigationMenu', () => {
|
|
204
|
+
const onNavigate = vi.fn();
|
|
205
|
+
|
|
206
|
+
renderWithProviders(
|
|
207
|
+
<Header
|
|
208
|
+
navItems={mockNavItems}
|
|
209
|
+
currentPath="/dashboard"
|
|
210
|
+
onNavigate={onNavigate}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(screen.getByTestId('navigation-menu')).toBeInTheDocument();
|
|
215
|
+
// NavigationMenu component should receive the props
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('renders all navigation items', () => {
|
|
219
|
+
renderWithProviders(<Header navItems={mockNavItems} />);
|
|
220
|
+
|
|
221
|
+
mockNavItems.forEach(item => {
|
|
222
|
+
expect(screen.getByTestId(`nav-item-${item.id}`)).toHaveTextContent(item.label);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// User menu tests
|
|
228
|
+
describe('User Menu', () => {
|
|
229
|
+
it('renders user menu when showUserMenu is true', () => {
|
|
230
|
+
renderWithProviders(<Header user={mockUser} showUserMenu={true} />);
|
|
231
|
+
|
|
232
|
+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not render user menu when showUserMenu is false', () => {
|
|
236
|
+
renderWithProviders(<Header user={mockUser} showUserMenu={false} />);
|
|
237
|
+
|
|
238
|
+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('renders user menu by default when user provided', () => {
|
|
242
|
+
renderWithProviders(<Header user={mockUser} />);
|
|
243
|
+
|
|
244
|
+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('displays user email in user menu trigger', () => {
|
|
248
|
+
renderWithProviders(<Header user={mockUser} />);
|
|
249
|
+
|
|
250
|
+
expect(screen.getByTestId('user-menu-trigger')).toHaveTextContent('test@example.com');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('displays sign in text when no user provided', () => {
|
|
254
|
+
renderWithProviders(<Header user={null} />);
|
|
255
|
+
|
|
256
|
+
expect(screen.getByTestId('user-menu-trigger')).toHaveTextContent('Sign In');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('renders custom user menu when provided', () => {
|
|
260
|
+
const CustomUserMenu = () => <div data-testid="custom-user-menu">Custom Menu</div>;
|
|
261
|
+
|
|
262
|
+
renderWithProviders(<Header user={mockUser} userMenu={<CustomUserMenu />} />);
|
|
263
|
+
|
|
264
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
265
|
+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('passes user and handlers to UserMenu', () => {
|
|
269
|
+
const onSignOut = vi.fn();
|
|
270
|
+
const onChangePassword = vi.fn();
|
|
271
|
+
|
|
272
|
+
renderWithProviders(
|
|
273
|
+
<Header
|
|
274
|
+
user={mockUser}
|
|
275
|
+
onSignOut={onSignOut}
|
|
276
|
+
onChangePassword={onChangePassword}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Event selector tests
|
|
285
|
+
describe('Event Selector', () => {
|
|
286
|
+
it('renders event selector when showEventSelector is true', () => {
|
|
287
|
+
renderWithProviders(<Header showEventSelector={true} />);
|
|
288
|
+
|
|
289
|
+
expect(screen.getByTestId('event-selector')).toBeInTheDocument();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('does not render event selector when showEventSelector is false', () => {
|
|
293
|
+
renderWithProviders(<Header showEventSelector={false} />);
|
|
294
|
+
|
|
295
|
+
expect(screen.queryByTestId('event-selector')).not.toBeInTheDocument();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('renders event selector by default', () => {
|
|
299
|
+
renderWithProviders(<Header />);
|
|
300
|
+
|
|
301
|
+
expect(screen.getByTestId('event-selector')).toBeInTheDocument();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('applies correct className to event selector', () => {
|
|
305
|
+
renderWithProviders(<Header showEventSelector={true} />);
|
|
306
|
+
|
|
307
|
+
const eventSelector = screen.getByTestId('event-selector');
|
|
308
|
+
expect(eventSelector).toHaveClass('justify-self-end', 'w-96');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('shows correct placeholder text', () => {
|
|
312
|
+
renderWithProviders(<Header showEventSelector={true} />);
|
|
313
|
+
|
|
314
|
+
expect(screen.getByRole('button', { name: 'Select event' })).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Custom actions tests
|
|
319
|
+
describe('Custom Actions', () => {
|
|
320
|
+
it('renders custom actions when provided', () => {
|
|
321
|
+
const customActions = (
|
|
322
|
+
<div data-testid="custom-actions">
|
|
323
|
+
<button>Export</button>
|
|
324
|
+
<button>New Item</button>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
renderWithProviders(<Header actions={customActions} />);
|
|
329
|
+
|
|
330
|
+
expect(screen.getByTestId('custom-actions')).toBeInTheDocument();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('does not render actions section when actions not provided', () => {
|
|
334
|
+
renderWithProviders(<Header />);
|
|
335
|
+
|
|
336
|
+
expect(screen.queryByTestId('custom-actions')).not.toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('renders multiple action buttons', () => {
|
|
340
|
+
const customActions = (
|
|
341
|
+
<div data-testid="custom-actions">
|
|
342
|
+
<button data-testid="export-btn">Export</button>
|
|
343
|
+
<button data-testid="new-btn">New Item</button>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
renderWithProviders(<Header actions={customActions} />);
|
|
348
|
+
|
|
349
|
+
expect(screen.getByTestId('export-btn')).toBeInTheDocument();
|
|
350
|
+
expect(screen.getByTestId('new-btn')).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Event handling tests
|
|
355
|
+
describe('Event Handling', () => {
|
|
356
|
+
it('handles navigation events', async () => {
|
|
357
|
+
const user = userEvent.setup();
|
|
358
|
+
const onNavigate = vi.fn();
|
|
359
|
+
|
|
360
|
+
renderWithProviders(
|
|
361
|
+
<Header
|
|
362
|
+
navItems={mockNavItems}
|
|
363
|
+
onNavigate={onNavigate}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// This would trigger navigation in real implementation
|
|
368
|
+
expect(screen.getByTestId('navigation-menu')).toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('handles sign out events', async () => {
|
|
372
|
+
const user = userEvent.setup();
|
|
373
|
+
const onSignOut = vi.fn().mockResolvedValue(undefined);
|
|
374
|
+
|
|
375
|
+
renderWithProviders(
|
|
376
|
+
<Header
|
|
377
|
+
user={mockUser}
|
|
378
|
+
onSignOut={onSignOut}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
await user.click(screen.getByTestId('sign-out-btn'));
|
|
383
|
+
|
|
384
|
+
expect(onSignOut).toHaveBeenCalledTimes(1);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('handles password change events', async () => {
|
|
388
|
+
const user = userEvent.setup();
|
|
389
|
+
const onChangePassword = vi.fn().mockResolvedValue({ error: null });
|
|
390
|
+
|
|
391
|
+
renderWithProviders(
|
|
392
|
+
<Header
|
|
393
|
+
user={mockUser}
|
|
394
|
+
onChangePassword={onChangePassword}
|
|
395
|
+
/>
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
await user.click(screen.getByTestId('change-password-btn'));
|
|
399
|
+
|
|
400
|
+
expect(onChangePassword).toHaveBeenCalledWith('new', 'new');
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Layout and responsive tests
|
|
405
|
+
describe('Layout and Responsive Design', () => {
|
|
406
|
+
it('applies correct grid layout classes', () => {
|
|
407
|
+
renderWithProviders(<Header />);
|
|
408
|
+
|
|
409
|
+
const nav = screen.getByRole('navigation');
|
|
410
|
+
expect(nav).toHaveClass('grid', 'grid-cols-[auto_auto_1fr_auto]', 'gap-4', 'h-full', 'items-center');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('applies correct container classes', () => {
|
|
414
|
+
renderWithProviders(<Header />);
|
|
415
|
+
|
|
416
|
+
const nav = screen.getByRole('navigation');
|
|
417
|
+
expect(nav).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('maintains proper header height', () => {
|
|
421
|
+
renderWithProviders(<Header />);
|
|
422
|
+
|
|
423
|
+
const header = screen.getByRole('banner');
|
|
424
|
+
expect(header).toHaveClass('h-16');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Accessibility tests
|
|
429
|
+
describe('Accessibility', () => {
|
|
430
|
+
it('has proper banner role', () => {
|
|
431
|
+
renderWithProviders(<Header />);
|
|
432
|
+
|
|
433
|
+
const header = screen.getByRole('banner');
|
|
434
|
+
expect(header).toBeInTheDocument();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('has proper navigation role', () => {
|
|
438
|
+
renderWithProviders(<Header />);
|
|
439
|
+
|
|
440
|
+
const nav = screen.getByRole('navigation');
|
|
441
|
+
expect(nav).toBeInTheDocument();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('provides proper alt text for logo', () => {
|
|
445
|
+
renderWithProviders(<Header logoUrl="/test-logo.svg" logoAlt="Test Logo" />);
|
|
446
|
+
|
|
447
|
+
const logo = screen.getByRole('img', { name: 'Test Logo' });
|
|
448
|
+
expect(logo).toHaveAttribute('alt', 'Test Logo');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('provides fallback alt text for default logo', () => {
|
|
452
|
+
renderWithProviders(<Header />);
|
|
453
|
+
|
|
454
|
+
const logo = screen.getByRole('img', { name: 'Logo' });
|
|
455
|
+
expect(logo).toHaveAttribute('alt', 'Logo');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Edge cases and error handling
|
|
460
|
+
describe('Edge Cases and Error Handling', () => {
|
|
461
|
+
it('handles null user gracefully', () => {
|
|
462
|
+
renderWithProviders(<Header user={null} />);
|
|
463
|
+
|
|
464
|
+
expect(screen.getByTestId('user-menu-trigger')).toHaveTextContent('Sign In');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('handles undefined user gracefully', () => {
|
|
468
|
+
renderWithProviders(<Header user={undefined} />);
|
|
469
|
+
|
|
470
|
+
expect(screen.getByTestId('user-menu-trigger')).toHaveTextContent('Sign In');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('handles empty navItems array', () => {
|
|
474
|
+
renderWithProviders(<Header navItems={[]} />);
|
|
475
|
+
|
|
476
|
+
expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('handles undefined navItems', () => {
|
|
480
|
+
renderWithProviders(<Header navItems={undefined} />);
|
|
481
|
+
|
|
482
|
+
expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('handles missing logoUrl gracefully', () => {
|
|
486
|
+
renderWithProviders(<Header logoAlt="Test Logo" />);
|
|
487
|
+
|
|
488
|
+
const logo = screen.getByRole('img', { name: 'Test Logo' });
|
|
489
|
+
expect(logo).toHaveAttribute('src', expect.stringContaining('data:image/svg+xml'));
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('handles missing logoAlt gracefully', () => {
|
|
493
|
+
renderWithProviders(<Header logoUrl="/test-logo.svg" />);
|
|
494
|
+
|
|
495
|
+
const logo = screen.getByRole('img', { name: 'Logo' });
|
|
496
|
+
expect(logo).toHaveAttribute('alt', 'Logo');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Integration tests
|
|
501
|
+
describe('Component Integration', () => {
|
|
502
|
+
it('renders all components together', () => {
|
|
503
|
+
const customActions = <div data-testid="custom-actions">Actions</div>;
|
|
504
|
+
|
|
505
|
+
renderWithProviders(
|
|
506
|
+
<Header
|
|
507
|
+
logoUrl="/test-logo.svg"
|
|
508
|
+
logoAlt="Test Logo"
|
|
509
|
+
navItems={mockNavItems}
|
|
510
|
+
user={mockUser}
|
|
511
|
+
actions={customActions}
|
|
512
|
+
showEventSelector={true}
|
|
513
|
+
showUserMenu={true}
|
|
514
|
+
/>
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Logo
|
|
518
|
+
expect(screen.getByRole('img', { name: 'Test Logo' })).toBeInTheDocument();
|
|
519
|
+
|
|
520
|
+
// Navigation
|
|
521
|
+
expect(screen.getByTestId('navigation-menu')).toBeInTheDocument();
|
|
522
|
+
|
|
523
|
+
// Event Selector
|
|
524
|
+
expect(screen.getByTestId('event-selector')).toBeInTheDocument();
|
|
525
|
+
|
|
526
|
+
// Custom Actions
|
|
527
|
+
expect(screen.getByTestId('custom-actions')).toBeInTheDocument();
|
|
528
|
+
|
|
529
|
+
// User Menu
|
|
530
|
+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('renders minimal configuration', () => {
|
|
534
|
+
renderWithProviders(
|
|
535
|
+
<Header
|
|
536
|
+
showEventSelector={false}
|
|
537
|
+
showUserMenu={false}
|
|
538
|
+
/>
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// Should only have logo and basic structure
|
|
542
|
+
expect(screen.getByRole('banner')).toBeInTheDocument();
|
|
543
|
+
expect(screen.getByRole('img', { name: 'Logo' })).toBeInTheDocument();
|
|
544
|
+
expect(screen.queryByTestId('navigation-menu')).not.toBeInTheDocument();
|
|
545
|
+
expect(screen.queryByTestId('event-selector')).not.toBeInTheDocument();
|
|
546
|
+
expect(screen.queryByTestId('user-menu')).not.toBeInTheDocument();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Performance tests
|
|
551
|
+
describe('Performance', () => {
|
|
552
|
+
it('renders quickly with all props', () => {
|
|
553
|
+
const startTime = performance.now();
|
|
554
|
+
|
|
555
|
+
renderWithProviders(
|
|
556
|
+
<Header
|
|
557
|
+
logoUrl="/test-logo.svg"
|
|
558
|
+
logoAlt="Test Logo"
|
|
559
|
+
navItems={mockNavItems}
|
|
560
|
+
user={mockUser}
|
|
561
|
+
actions={<div>Actions</div>}
|
|
562
|
+
showEventSelector={true}
|
|
563
|
+
showUserMenu={true}
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const endTime = performance.now();
|
|
568
|
+
expect(endTime - startTime).toBeLessThan(100); // Should render in under 100ms
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('handles rapid re-renders', () => {
|
|
572
|
+
const { rerender } = renderWithProviders(<Header user={mockUser} />);
|
|
573
|
+
|
|
574
|
+
// Rapid re-renders with different props
|
|
575
|
+
for (let i = 0; i < 10; i++) {
|
|
576
|
+
rerender(<Header user={mockUser} currentPath={`/path-${i}`} />);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
expect(screen.getByTestId('user-menu')).toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|