@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.
Files changed (210) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -55
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /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
+ });
@@ -273,7 +273,7 @@ export function Header({
273
273
  {showEventSelector && (
274
274
  <EventSelector
275
275
  placeholder="Select event"
276
- className="justify-self-end hidden sm:block w-96"
276
+ className="justify-self-end w-96"
277
277
  data-testid="event-selector"
278
278
  />
279
279
  )}