@jmruthers/pace-core 0.5.75 → 0.5.76

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 (226) hide show
  1. package/dist/{DataTable-HWZQGASI.js → DataTable-4GAVPIEG.js} +48 -30
  2. package/dist/{PublicLoadingSpinner-BKNBT6b6.d.ts → PublicLoadingSpinner-BiNER8F5.d.ts} +28 -17
  3. package/dist/{chunk-33PHABLB.js → chunk-AFGTSUAD.js} +10 -127
  4. package/dist/chunk-AFGTSUAD.js.map +1 -0
  5. package/dist/{chunk-2DFZ432F.js → chunk-K34IM5CT.js} +3 -5
  6. package/dist/{chunk-2DFZ432F.js.map → chunk-K34IM5CT.js.map} +1 -1
  7. package/dist/{chunk-2CHATWBF.js → chunk-KHJS6VIA.js} +199 -35
  8. package/dist/chunk-KHJS6VIA.js.map +1 -0
  9. package/dist/{chunk-ZTT2AXMX.js → chunk-KK73ZB4E.js} +2 -2
  10. package/dist/{chunk-CY3AHGO4.js → chunk-M5IWZRBT.js} +1750 -2815
  11. package/dist/chunk-M5IWZRBT.js.map +1 -0
  12. package/dist/{chunk-DAXLNIDY.js → chunk-Y6TXWPJO.js} +6 -4
  13. package/dist/{chunk-DAXLNIDY.js.map → chunk-Y6TXWPJO.js.map} +1 -1
  14. package/dist/{chunk-YNUBMSMV.js → chunk-YCKPEMJA.js} +186 -263
  15. package/dist/chunk-YCKPEMJA.js.map +1 -0
  16. package/dist/components.d.ts +1 -1
  17. package/dist/components.js +7 -6
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.d.ts +17 -40
  20. package/dist/hooks.js +6 -6
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +12 -10
  23. package/dist/index.js.map +1 -1
  24. package/dist/rbac/index.d.ts +54 -1
  25. package/dist/rbac/index.js +5 -4
  26. package/dist/utils.js +1 -1
  27. package/docs/TERMINOLOGY.md +231 -0
  28. package/docs/api/classes/ColumnFactory.md +1 -1
  29. package/docs/api/classes/ErrorBoundary.md +1 -1
  30. package/docs/api/classes/InvalidScopeError.md +1 -1
  31. package/docs/api/classes/MissingUserContextError.md +1 -1
  32. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  33. package/docs/api/classes/PermissionDeniedError.md +1 -1
  34. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  35. package/docs/api/classes/RBACAuditManager.md +1 -1
  36. package/docs/api/classes/RBACCache.md +1 -1
  37. package/docs/api/classes/RBACEngine.md +1 -1
  38. package/docs/api/classes/RBACError.md +1 -1
  39. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  40. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  41. package/docs/api/classes/StorageUtils.md +1 -1
  42. package/docs/api/enums/FileCategory.md +1 -1
  43. package/docs/api/interfaces/AggregateConfig.md +1 -1
  44. package/docs/api/interfaces/ButtonProps.md +1 -1
  45. package/docs/api/interfaces/CardProps.md +1 -1
  46. package/docs/api/interfaces/ColorPalette.md +1 -1
  47. package/docs/api/interfaces/ColorShade.md +1 -1
  48. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  49. package/docs/api/interfaces/DataTableAction.md +1 -1
  50. package/docs/api/interfaces/DataTableColumn.md +1 -1
  51. package/docs/api/interfaces/DataTableProps.md +1 -1
  52. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  53. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  54. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/EventLogoProps.md +1 -1
  56. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  57. package/docs/api/interfaces/FileMetadata.md +1 -1
  58. package/docs/api/interfaces/FileReference.md +1 -1
  59. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  60. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  61. package/docs/api/interfaces/FileUploadProps.md +1 -1
  62. package/docs/api/interfaces/FooterProps.md +1 -1
  63. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  64. package/docs/api/interfaces/InputProps.md +1 -1
  65. package/docs/api/interfaces/LabelProps.md +1 -1
  66. package/docs/api/interfaces/LoginFormProps.md +1 -1
  67. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  68. package/docs/api/interfaces/NavigationContextType.md +1 -1
  69. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  70. package/docs/api/interfaces/NavigationItem.md +1 -1
  71. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  73. package/docs/api/interfaces/Organisation.md +1 -1
  74. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  75. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  76. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  77. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  78. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  79. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  80. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  81. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  82. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  83. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  84. package/docs/api/interfaces/PaletteData.md +1 -1
  85. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  86. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  87. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  88. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  89. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  92. package/docs/api/interfaces/RBACConfig.md +1 -1
  93. package/docs/api/interfaces/RBACContextType.md +1 -1
  94. package/docs/api/interfaces/RBACLogger.md +1 -1
  95. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  97. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  98. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  99. package/docs/api/interfaces/RouteConfig.md +1 -1
  100. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  101. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  102. package/docs/api/interfaces/StorageConfig.md +1 -1
  103. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  104. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  105. package/docs/api/interfaces/StorageListOptions.md +1 -1
  106. package/docs/api/interfaces/StorageListResult.md +1 -1
  107. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  108. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  109. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  110. package/docs/api/interfaces/StyleImport.md +1 -1
  111. package/docs/api/interfaces/SwitchProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +47 -0
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +47 -0
  125. package/docs/api/interfaces/UserEventAccess.md +1 -1
  126. package/docs/api/interfaces/UserMenuProps.md +1 -1
  127. package/docs/api/interfaces/UserProfile.md +1 -1
  128. package/docs/api/modules.md +57 -11
  129. package/docs/api-reference/providers.md +26 -7
  130. package/docs/best-practices/README.md +20 -0
  131. package/docs/best-practices/accessibility.md +566 -0
  132. package/docs/best-practices/performance-expansion.md +473 -0
  133. package/docs/core-concepts/authentication.md +15 -7
  134. package/docs/documentation-index.md +1 -1
  135. package/docs/documentation-templates.md +539 -0
  136. package/docs/getting-started/quick-start.md +16 -66
  137. package/docs/implementation-guides/component-styling.md +410 -0
  138. package/docs/implementation-guides/data-tables.md +1 -1
  139. package/docs/style-guide.md +39 -0
  140. package/package.json +1 -1
  141. package/src/__tests__/TEST_GUIDE_CURSOR.md +290 -0
  142. package/src/__tests__/helpers/supabaseMock.ts +48 -2
  143. package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +17 -6
  144. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +73 -9
  145. package/src/components/DataTable/components/DataTableCore.tsx +280 -475
  146. package/src/components/DataTable/components/UnifiedTableBody.tsx +120 -153
  147. package/src/components/DataTable/components/index.ts +1 -2
  148. package/src/components/DataTable/context/__tests__/DataTableContext.test.tsx +208 -275
  149. package/src/components/DataTable/core/index.ts +1 -8
  150. package/src/components/DataTable/hooks/__tests__/useColumnOrderPersistence.test.ts +525 -0
  151. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +570 -0
  152. package/src/components/DataTable/hooks/__tests__/useHierarchicalState.test.ts +214 -0
  153. package/src/components/DataTable/hooks/__tests__/useTableColumns.test.ts +224 -0
  154. package/src/components/DataTable/hooks/index.ts +6 -0
  155. package/src/components/DataTable/hooks/useColumnReordering.ts +1 -0
  156. package/src/components/DataTable/hooks/useDataTablePermissions.ts +149 -0
  157. package/src/components/DataTable/hooks/useDataTableState.ts +12 -6
  158. package/src/components/DataTable/hooks/useHierarchicalState.ts +26 -8
  159. package/src/components/DataTable/hooks/useTableColumns.ts +153 -0
  160. package/src/components/DataTable/index.ts +1 -9
  161. package/src/components/DataTable/utils/__tests__/COVERAGE_NOTE.md +89 -0
  162. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +3 -6
  163. package/src/components/DataTable/utils/__tests__/flexibleImport.test.ts +462 -0
  164. package/src/components/DataTable/utils/__tests__/hierarchicalSorting.test.ts +247 -0
  165. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +8 -6
  166. package/src/components/DataTable/utils/__tests__/performanceUtils.test.ts +466 -0
  167. package/src/components/DataTable/utils/__tests__/rowUtils.test.ts +265 -0
  168. package/src/components/DataTable/utils/errorHandling.ts +52 -460
  169. package/src/components/DataTable/utils/exportUtils.ts +46 -15
  170. package/src/components/DataTable/utils/hierarchicalSorting.ts +50 -3
  171. package/src/components/DataTable/utils/hierarchicalUtils.ts +167 -34
  172. package/src/components/DataTable/utils/index.ts +5 -0
  173. package/src/components/DataTable/utils/rowUtils.ts +68 -0
  174. package/src/components/EventSelector/EventSelector.test.tsx +672 -0
  175. package/src/components/Label/__tests__/Label.test.tsx +434 -0
  176. package/src/components/PublicLayout/__tests__/PublicPageContextChecker.test.tsx +190 -0
  177. package/src/components/PublicLayout/__tests__/PublicPageDebugger.test.tsx +185 -0
  178. package/src/components/PublicLayout/__tests__/PublicPageProvider.test.tsx +313 -0
  179. package/src/components/Select/Select.test.tsx +143 -120
  180. package/src/components/Select/Select.tsx +47 -212
  181. package/src/components/Select/hooks.ts +36 -1
  182. package/src/components/Select/index.ts +2 -1
  183. package/src/hooks/services/__tests__/useServiceHooks.test.tsx +137 -0
  184. package/src/hooks/useSecureDataAccess.test.ts +32 -29
  185. package/src/providers/__tests__/ProviderLifecycle.test.tsx +341 -0
  186. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +437 -0
  187. package/src/rbac/hooks/index.ts +2 -0
  188. package/src/rbac/hooks/useResolvedScope.ts +232 -0
  189. package/src/services/__tests__/InactivityService.lifecycle.test.ts +411 -0
  190. package/src/services/__tests__/OrganisationService.pagination.test.ts +375 -0
  191. package/src/types/__tests__/README.md +114 -0
  192. package/src/types/__tests__/validation.test.ts +731 -0
  193. package/src/utils/__tests__/file-reference.test.ts +383 -0
  194. package/src/utils/__tests__/performanceBenchmark.test.ts +175 -0
  195. package/src/utils/appNameResolver.test.ts +54 -0
  196. package/src/validation/__tests__/csrf.unit.test.ts +63 -0
  197. package/src/validation/__tests__/passwordSchema.unit.test.ts +105 -0
  198. package/dist/chunk-2CHATWBF.js.map +0 -1
  199. package/dist/chunk-33PHABLB.js.map +0 -1
  200. package/dist/chunk-CY3AHGO4.js.map +0 -1
  201. package/dist/chunk-TYHR5X4W.js +0 -33
  202. package/dist/chunk-TYHR5X4W.js.map +0 -1
  203. package/dist/chunk-YNUBMSMV.js.map +0 -1
  204. package/dist/eventContext-BBA42P6G.js +0 -14
  205. package/dist/eventContext-BBA42P6G.js.map +0 -1
  206. package/docs/documentation-style-checklist.md +0 -294
  207. package/src/components/DataTable/components/DataTableBody.tsx +0 -488
  208. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -144
  209. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -515
  210. package/src/components/DataTable/core/ActionManager.ts +0 -235
  211. package/src/components/DataTable/core/ColumnManager.ts +0 -215
  212. package/src/components/DataTable/core/DataManager.ts +0 -188
  213. package/src/components/DataTable/core/DataTableContext.tsx +0 -181
  214. package/src/components/DataTable/core/LocalDataAdapter.ts +0 -264
  215. package/src/components/DataTable/core/PluginRegistry.ts +0 -229
  216. package/src/components/DataTable/core/StateManager.ts +0 -311
  217. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +0 -634
  218. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +0 -193
  219. package/src/components/DataTable/core/__tests__/DataManager.test.ts +0 -519
  220. package/src/components/DataTable/core/__tests__/StateManager.test.ts +0 -714
  221. package/src/components/DataTable/core/interfaces.ts +0 -338
  222. package/src/components/DataTable/utils/debugTools.ts +0 -583
  223. package/src/components/Select/Select.bug-test.tsx +0 -69
  224. package/src/components/Select/Select.refactored.tsx +0 -497
  225. /package/dist/{DataTable-HWZQGASI.js.map → DataTable-4GAVPIEG.js.map} +0 -0
  226. /package/dist/{chunk-ZTT2AXMX.js.map → chunk-KK73ZB4E.js.map} +0 -0
@@ -0,0 +1,672 @@
1
+ /**
2
+ * @file EventSelector Component Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Components/EventSelector
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive test suite for the EventSelector component covering all functionality,
8
+ * event selection, error handling, and accessibility features.
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 { EventSelector, EventSelectorProps } from './EventSelector';
16
+ import { renderWithProviders } from '../../__tests__/helpers/test-utils';
17
+ import type { Event } from '../../types';
18
+
19
+ // Mock the useEvents hook
20
+ const mockUseEvents = vi.fn();
21
+ vi.mock('../../hooks/useEvents', () => ({
22
+ useEvents: () => mockUseEvents(),
23
+ }));
24
+
25
+ // Mock child components
26
+ let mockOnValueChange: ((value: string) => void) | null = null;
27
+
28
+ vi.mock('../Select', () => ({
29
+ Select: ({ children, value, onValueChange, className }: any) => {
30
+ // Store the onValueChange callback for SelectItem to use
31
+ mockOnValueChange = onValueChange;
32
+ return (
33
+ <div data-testid="select" data-value={value} className={className}>
34
+ {children}
35
+ </div>
36
+ );
37
+ },
38
+ SelectContent: ({ children }: any) => (
39
+ <div data-testid="select-content">{children}</div>
40
+ ),
41
+ SelectItem: ({ children, value, className }: any) => (
42
+ <div
43
+ data-testid={`select-item-${value}`}
44
+ className={className}
45
+ onClick={() => mockOnValueChange?.(value)}
46
+ >
47
+ {children}
48
+ </div>
49
+ ),
50
+ SelectTrigger: ({ children, className }: any) => (
51
+ <button data-testid="select-trigger" className={className}>
52
+ {children}
53
+ </button>
54
+ ),
55
+ SelectValue: ({ placeholder, children }: any) => (
56
+ <span data-testid="select-value">
57
+ {children || placeholder}
58
+ </span>
59
+ ),
60
+ }));
61
+
62
+ vi.mock('../Alert/Alert', () => ({
63
+ Alert: ({ children, variant }: any) => (
64
+ <div data-testid="alert" data-variant={variant}>
65
+ {children}
66
+ </div>
67
+ ),
68
+ AlertDescription: ({ children, className }: any) => (
69
+ <div data-testid="alert-description" className={className}>
70
+ {children}
71
+ </div>
72
+ ),
73
+ }));
74
+
75
+ vi.mock('../Button/Button', () => ({
76
+ Button: ({ children, onClick, variant, size, className }: any) => (
77
+ <button
78
+ data-testid="button"
79
+ onClick={onClick}
80
+ data-variant={variant}
81
+ data-size={size}
82
+ className={className}
83
+ >
84
+ {children}
85
+ </button>
86
+ ),
87
+ }));
88
+
89
+ vi.mock('../LoadingSpinner/LoadingSpinner', () => ({
90
+ LoadingSpinner: ({ size }: any) => (
91
+ <div data-testid="loading-spinner" data-size={size}>Loading...</div>
92
+ ),
93
+ }));
94
+
95
+ vi.mock('lucide-react', () => ({
96
+ RefreshCw: () => <span data-testid="icon-refresh">refresh</span>,
97
+ AlertCircle: () => <span data-testid="icon-alert">alert</span>,
98
+ Lock: () => <span data-testid="icon-lock">lock</span>,
99
+ Calendar: () => <span data-testid="icon-calendar">calendar</span>,
100
+ Star: () => <span data-testid="icon-star">star</span>,
101
+ }));
102
+
103
+ // Test data
104
+ const mockEvents: Event[] = [
105
+ {
106
+ id: 'event-1',
107
+ event_id: 'event-1',
108
+ event_name: 'Spring Festival 2024',
109
+ event_date: '2024-03-15T00:00:00Z',
110
+ event_venue: 'City Park',
111
+ organisation_id: 'org-1',
112
+ created_at: '2023-01-01T00:00:00Z',
113
+ updated_at: '2023-01-01T00:00:00Z',
114
+ },
115
+ {
116
+ id: 'event-2',
117
+ event_id: 'event-2',
118
+ event_name: 'Summer Conference',
119
+ event_date: '2024-06-20T00:00:00Z',
120
+ event_venue: 'Convention Center',
121
+ organisation_id: 'org-1',
122
+ created_at: '2023-01-02T00:00:00Z',
123
+ updated_at: '2023-01-02T00:00:00Z',
124
+ },
125
+ {
126
+ id: 'event-3',
127
+ event_id: 'event-3',
128
+ event_name: 'Winter Gala',
129
+ event_date: '2024-12-01T00:00:00Z',
130
+ event_venue: 'Grand Hotel',
131
+ organisation_id: 'org-1',
132
+ created_at: '2023-01-03T00:00:00Z',
133
+ updated_at: '2023-01-03T00:00:00Z',
134
+ },
135
+ ];
136
+
137
+ const mockSelectedEvent = mockEvents[0];
138
+
139
+ const defaultMockContext = {
140
+ events: mockEvents,
141
+ selectedEvent: mockSelectedEvent,
142
+ isLoading: false,
143
+ error: null,
144
+ setSelectedEvent: vi.fn(),
145
+ refreshEvents: vi.fn().mockResolvedValue(undefined),
146
+ };
147
+
148
+ describe('EventSelector Component', () => {
149
+ beforeEach(() => {
150
+ vi.clearAllMocks();
151
+
152
+ // Setup the default mock context
153
+ mockUseEvents.mockReturnValue(defaultMockContext);
154
+ });
155
+
156
+ afterEach(() => {
157
+ mockOnValueChange = null;
158
+ });
159
+
160
+ describe('Rendering', () => {
161
+ it('renders with default props', () => {
162
+ renderWithProviders(<EventSelector />);
163
+
164
+ expect(screen.getByTestId('select')).toBeInTheDocument();
165
+ expect(screen.getByTestId('select-trigger')).toBeInTheDocument();
166
+ });
167
+
168
+ it('renders with custom placeholder', () => {
169
+ mockUseEvents.mockReturnValue({
170
+ ...defaultMockContext,
171
+ selectedEvent: null, // No selected event should show placeholder
172
+ });
173
+
174
+ renderWithProviders(<EventSelector placeholder="Choose an event" />);
175
+
176
+ expect(screen.getByTestId('select-value')).toBeInTheDocument();
177
+ });
178
+
179
+ it('renders with custom className', () => {
180
+ const { container } = renderWithProviders(<EventSelector className="custom-class" />);
181
+
182
+ const select = screen.getByTestId('select');
183
+ expect(select).toHaveClass('custom-class');
184
+ });
185
+
186
+ it('renders all events in dropdown', () => {
187
+ renderWithProviders(<EventSelector />);
188
+
189
+ expect(screen.getByTestId('select-content')).toBeInTheDocument();
190
+ });
191
+ });
192
+
193
+ describe('Loading State', () => {
194
+ it('shows loading spinner when isLoading is true', () => {
195
+ mockUseEvents.mockReturnValue({
196
+ ...defaultMockContext,
197
+ isLoading: true,
198
+ });
199
+
200
+ renderWithProviders(<EventSelector />);
201
+
202
+ expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
203
+ expect(screen.getByText('Loading events...')).toBeInTheDocument();
204
+ });
205
+ });
206
+
207
+ describe('Error State', () => {
208
+ it('shows error message when error occurs', () => {
209
+ const error = new Error('Failed to load events');
210
+ mockUseEvents.mockReturnValue({
211
+ ...defaultMockContext,
212
+ error,
213
+ });
214
+
215
+ renderWithProviders(<EventSelector />);
216
+
217
+ expect(screen.getByTestId('alert')).toBeInTheDocument();
218
+ expect(screen.getByTestId('alert')).toHaveAttribute('data-variant', 'destructive');
219
+ expect(screen.getByText('Failed to load events')).toBeInTheDocument();
220
+ });
221
+
222
+ it('shows retry button when error and showRetryButton is true', () => {
223
+ const error = new Error('Failed to load events');
224
+ mockUseEvents.mockReturnValue({
225
+ ...defaultMockContext,
226
+ error,
227
+ });
228
+
229
+ renderWithProviders(<EventSelector showRetryButton={true} />);
230
+
231
+ expect(screen.getByTestId('button')).toBeInTheDocument();
232
+ expect(screen.getByText('Retry')).toBeInTheDocument();
233
+ });
234
+
235
+ it('hides retry button when showRetryButton is false', () => {
236
+ const error = new Error('Failed to load events');
237
+ mockUseEvents.mockReturnValue({
238
+ ...defaultMockContext,
239
+ error,
240
+ });
241
+
242
+ renderWithProviders(<EventSelector showRetryButton={false} />);
243
+
244
+ expect(screen.queryByTestId('button')).not.toBeInTheDocument();
245
+ });
246
+
247
+ it('calls refreshEvents when retry button is clicked', async () => {
248
+ const refreshEventsMock = vi.fn().mockResolvedValue(undefined);
249
+ const error = new Error('Failed to load events');
250
+
251
+ mockUseEvents.mockReturnValue({
252
+ ...defaultMockContext,
253
+ error,
254
+ refreshEvents: refreshEventsMock,
255
+ });
256
+
257
+ renderWithProviders(<EventSelector showRetryButton={true} />);
258
+
259
+ const retryButton = screen.getByTestId('button');
260
+ const user = userEvent.setup();
261
+ await user.click(retryButton);
262
+
263
+ expect(refreshEventsMock).toHaveBeenCalledTimes(1);
264
+ });
265
+ });
266
+
267
+ describe('No Events State', () => {
268
+ it('shows no events message when showNoEventsMessage is true and no events', () => {
269
+ mockUseEvents.mockReturnValue({
270
+ ...defaultMockContext,
271
+ events: [],
272
+ });
273
+
274
+ renderWithProviders(<EventSelector showNoEventsMessage={true} />);
275
+
276
+ expect(screen.getByTestId('alert')).toBeInTheDocument();
277
+ expect(screen.getByText('No events available.')).toBeInTheDocument();
278
+ });
279
+
280
+ it('hides message when showNoEventsMessage is false and no events', () => {
281
+ mockUseEvents.mockReturnValue({
282
+ ...defaultMockContext,
283
+ events: [],
284
+ });
285
+
286
+ const { container } = renderWithProviders(<EventSelector showNoEventsMessage={false} />);
287
+
288
+ expect(container.firstChild).toBeNull();
289
+ });
290
+
291
+ it('shows refresh button when no events and showRetryButton is true', () => {
292
+ mockUseEvents.mockReturnValue({
293
+ ...defaultMockContext,
294
+ events: [],
295
+ });
296
+
297
+ renderWithProviders(<EventSelector showRetryButton={true} />);
298
+
299
+ expect(screen.getByTestId('button')).toBeInTheDocument();
300
+ expect(screen.getByText('Refresh')).toBeInTheDocument();
301
+ });
302
+ });
303
+
304
+ describe('Event Selection', () => {
305
+ it('calls onEventChange when an event is selected', async () => {
306
+ const onEventChangeMock = vi.fn();
307
+
308
+ renderWithProviders(
309
+ <EventSelector onEventChange={onEventChangeMock} />
310
+ );
311
+
312
+ // Find and click an event item
313
+ const user = userEvent.setup();
314
+ const eventItem = screen.getByTestId('select-item-event-2');
315
+ await user.click(eventItem);
316
+
317
+ expect(onEventChangeMock).toHaveBeenCalledTimes(1);
318
+ });
319
+
320
+ it('calls setSelectedEvent when an event is selected', async () => {
321
+ const setSelectedEventMock = vi.fn();
322
+
323
+ mockUseEvents.mockReturnValue({
324
+ ...defaultMockContext,
325
+ setSelectedEvent: setSelectedEventMock,
326
+ });
327
+
328
+ renderWithProviders(<EventSelector />);
329
+
330
+ const user = userEvent.setup();
331
+ const eventItem = screen.getByTestId('select-item-event-2');
332
+ await user.click(eventItem);
333
+
334
+ expect(setSelectedEventMock).toHaveBeenCalled();
335
+ });
336
+
337
+ it('displays selected event name and date', () => {
338
+ renderWithProviders(<EventSelector />);
339
+
340
+ expect(screen.getByTestId('select')).toHaveAttribute('data-value', 'event-1');
341
+ });
342
+ });
343
+
344
+ describe('Event Sorting', () => {
345
+ it('sorts events by date descending (newest first)', () => {
346
+ const unsortedEvents: Event[] = [
347
+ {
348
+ id: 'event-old',
349
+ event_id: 'event-old',
350
+ event_name: 'Old Event',
351
+ event_date: '2024-01-01T00:00:00Z',
352
+ organisation_id: 'org-1',
353
+ created_at: '2023-01-01T00:00:00Z',
354
+ updated_at: '2023-01-01T00:00:00Z',
355
+ },
356
+ {
357
+ id: 'event-new',
358
+ event_id: 'event-new',
359
+ event_name: 'New Event',
360
+ event_date: '2024-12-31T00:00:00Z',
361
+ organisation_id: 'org-1',
362
+ created_at: '2023-01-02T00:00:00Z',
363
+ updated_at: '2023-01-02T00:00:00Z',
364
+ },
365
+ ];
366
+
367
+ mockUseEvents.mockReturnValue({
368
+ ...defaultMockContext,
369
+ events: unsortedEvents,
370
+ });
371
+
372
+ renderWithProviders(<EventSelector />);
373
+
374
+ // The newest event should appear first in the DOM
375
+ const items = screen.getAllByTestId(/^select-item-/);
376
+ expect(items[0]).toHaveAttribute('data-testid', 'select-item-event-new');
377
+ });
378
+ });
379
+
380
+ describe('Auto-Selection', () => {
381
+ it('auto-selects next upcoming event when no event is selected', () => {
382
+ const today = new Date('2024-03-10T00:00:00Z');
383
+ vi.useFakeTimers({ now: today });
384
+
385
+ const setSelectedEventMock = vi.fn();
386
+ const onEventChangeMock = vi.fn();
387
+
388
+ mockUseEvents.mockReturnValue({
389
+ ...defaultMockContext,
390
+ selectedEvent: null,
391
+ setSelectedEvent: setSelectedEventMock,
392
+ });
393
+
394
+ renderWithProviders(<EventSelector onEventChange={onEventChangeMock} />);
395
+
396
+ // Should auto-select the next event (event-1 with date 2024-03-15)
397
+ expect(setSelectedEventMock).toHaveBeenCalled();
398
+ });
399
+ });
400
+
401
+ describe('Event Details Display', () => {
402
+ it('shows event details when showEventDetails is true', () => {
403
+ renderWithProviders(<EventSelector showEventDetails={true} />);
404
+
405
+ expect(screen.getByTestId('select-content')).toBeInTheDocument();
406
+ });
407
+
408
+ it('hides event details when showEventDetails is false', () => {
409
+ renderWithProviders(<EventSelector showEventDetails={false} />);
410
+
411
+ // Details should not be visible in the rendered content
412
+ const content = screen.getByTestId('select-content');
413
+ expect(content).toBeInTheDocument();
414
+ });
415
+ });
416
+
417
+ describe('Next Event Indicator', () => {
418
+ it('shows next event indicator when showNextEventIndicator is true', () => {
419
+ const today = new Date('2024-03-10T12:00:00Z');
420
+ vi.useFakeTimers({ now: today });
421
+
422
+ renderWithProviders(<EventSelector showNextEventIndicator={true} />);
423
+
424
+ // Should render the star icon for next events (events-1, 2, 3 are in the future)
425
+ expect(screen.getAllByTestId('icon-star').length).toBeGreaterThan(0);
426
+ });
427
+
428
+ it('hides next event indicator when showNextEventIndicator is false', () => {
429
+ renderWithProviders(<EventSelector showNextEventIndicator={false} />);
430
+
431
+ // No star icons should be present
432
+ expect(screen.queryByTestId('icon-star')).not.toBeInTheDocument();
433
+ });
434
+ });
435
+
436
+ describe('Format Event Date', () => {
437
+ it('formats today\'s date as "Today"', () => {
438
+ const today = new Date('2024-03-15T12:00:00Z');
439
+ vi.useFakeTimers({ now: today });
440
+
441
+ const todayEvent: Event[] = [{
442
+ id: 'event-today',
443
+ event_id: 'event-today',
444
+ event_name: 'Today Event',
445
+ event_date: '2024-03-15T00:00:00Z',
446
+ organisation_id: 'org-1',
447
+ created_at: '2024-01-01T00:00:00Z',
448
+ updated_at: '2024-01-01T00:00:00Z',
449
+ }];
450
+
451
+ mockUseEvents.mockReturnValue({
452
+ ...defaultMockContext,
453
+ events: todayEvent,
454
+ selectedEvent: todayEvent[0],
455
+ });
456
+
457
+ renderWithProviders(<EventSelector />);
458
+
459
+ // Should display "Today" for today's event
460
+ expect(screen.getByText('Today')).toBeInTheDocument();
461
+ });
462
+
463
+ it('formats tomorrow\'s date as "Tomorrow"', () => {
464
+ const today = new Date('2024-03-15T12:00:00Z');
465
+ vi.useFakeTimers({ now: today });
466
+
467
+ const tomorrowEvent: Event[] = [{
468
+ id: 'event-tomorrow',
469
+ event_id: 'event-tomorrow',
470
+ event_name: 'Tomorrow Event',
471
+ event_date: '2024-03-16T00:00:00Z',
472
+ organisation_id: 'org-1',
473
+ created_at: '2024-01-01T00:00:00Z',
474
+ updated_at: '2024-01-01T00:00:00Z',
475
+ }];
476
+
477
+ mockUseEvents.mockReturnValue({
478
+ ...defaultMockContext,
479
+ events: tomorrowEvent,
480
+ selectedEvent: tomorrowEvent[0],
481
+ });
482
+
483
+ renderWithProviders(<EventSelector />);
484
+
485
+ // Should display "Tomorrow" for tomorrow's event
486
+ expect(screen.getByText('Tomorrow')).toBeInTheDocument();
487
+ });
488
+
489
+ it('formats past/future dates as locale date string', () => {
490
+ const today = new Date('2024-03-15T12:00:00Z');
491
+ vi.useFakeTimers({ now: today });
492
+
493
+ const futureEvent: Event[] = [{
494
+ id: 'event-future',
495
+ event_id: 'event-future',
496
+ event_name: 'Future Event',
497
+ event_date: '2024-06-20T00:00:00Z',
498
+ organisation_id: 'org-1',
499
+ created_at: '2024-01-01T00:00:00Z',
500
+ updated_at: '2024-01-01T00:00:00Z',
501
+ }];
502
+
503
+ mockUseEvents.mockReturnValue({
504
+ ...defaultMockContext,
505
+ events: futureEvent,
506
+ selectedEvent: futureEvent[0],
507
+ });
508
+
509
+ renderWithProviders(<EventSelector />);
510
+
511
+ // Should display formatted date
512
+ const dateString = new Date('2024-06-20T00:00:00Z').toLocaleDateString();
513
+ expect(screen.getByText(dateString)).toBeInTheDocument();
514
+ });
515
+ });
516
+
517
+ describe('Is Next Event', () => {
518
+ it('identifies events on or after today as next events', () => {
519
+ const today = new Date('2024-03-15T12:00:00Z');
520
+ vi.useFakeTimers({ now: today });
521
+
522
+ const futureEvent: Event[] = [{
523
+ id: 'event-future',
524
+ event_id: 'event-future',
525
+ event_name: 'Future Event',
526
+ event_date: '2024-06-20T00:00:00Z',
527
+ organisation_id: 'org-1',
528
+ created_at: '2024-01-01T00:00:00Z',
529
+ updated_at: '2024-01-01T00:00:00Z',
530
+ }];
531
+
532
+ mockUseEvents.mockReturnValue({
533
+ ...defaultMockContext,
534
+ events: futureEvent,
535
+ });
536
+
537
+ renderWithProviders(<EventSelector showNextEventIndicator={true} />);
538
+
539
+ // Should show star indicator for future events
540
+ expect(screen.getByTestId('icon-star')).toBeInTheDocument();
541
+ });
542
+
543
+ it('does not identify past events as next events', () => {
544
+ const today = new Date('2024-03-15T12:00:00Z');
545
+ vi.useFakeTimers({ now: today });
546
+
547
+ const pastEvent: Event[] = [{
548
+ id: 'event-past',
549
+ event_id: 'event-past',
550
+ event_name: 'Past Event',
551
+ event_date: '2024-01-01T00:00:00Z',
552
+ organisation_id: 'org-1',
553
+ created_at: '2024-01-01T00:00:00Z',
554
+ updated_at: '2024-01-01T00:00:00Z',
555
+ }];
556
+
557
+ mockUseEvents.mockReturnValue({
558
+ ...defaultMockContext,
559
+ events: pastEvent,
560
+ });
561
+
562
+ renderWithProviders(<EventSelector showNextEventIndicator={true} />);
563
+
564
+ // Should not show star indicator for past events
565
+ expect(screen.queryByTestId('icon-star')).not.toBeInTheDocument();
566
+ });
567
+ });
568
+
569
+ describe('Integration with useEvents Hook', () => {
570
+ it('uses events from useEvents hook', () => {
571
+ const customEvents: Event[] = [
572
+ {
573
+ id: 'custom-event',
574
+ event_id: 'custom-event',
575
+ event_name: 'Custom Event',
576
+ event_date: '2024-12-01T00:00:00Z',
577
+ organisation_id: 'org-1',
578
+ created_at: '2024-01-01T00:00:00Z',
579
+ updated_at: '2024-01-01T00:00:00Z',
580
+ },
581
+ ];
582
+
583
+ mockUseEvents.mockReturnValue({
584
+ ...defaultMockContext,
585
+ events: customEvents,
586
+ });
587
+
588
+ renderWithProviders(<EventSelector />);
589
+
590
+ expect(mockUseEvents).toHaveBeenCalled();
591
+ });
592
+
593
+ it('calls setSelectedEvent from useEvents hook when event changes', async () => {
594
+ const setSelectedEventMock = vi.fn();
595
+
596
+ mockUseEvents.mockReturnValue({
597
+ ...defaultMockContext,
598
+ setSelectedEvent: setSelectedEventMock,
599
+ });
600
+
601
+ renderWithProviders(<EventSelector />);
602
+
603
+ const user = userEvent.setup();
604
+ const eventItem = screen.getByTestId('select-item-event-2');
605
+ await user.click(eventItem);
606
+
607
+ expect(setSelectedEventMock).toHaveBeenCalled();
608
+ });
609
+ });
610
+
611
+ describe('Edge Cases', () => {
612
+ it('handles events without event_date gracefully', () => {
613
+ const eventsWithoutDate: Event[] = [{
614
+ id: 'no-date-event',
615
+ event_id: 'no-date-event',
616
+ event_name: 'Event Without Date',
617
+ organisation_id: 'org-1',
618
+ created_at: '2024-01-01T00:00:00Z',
619
+ updated_at: '2024-01-01T00:00:00Z',
620
+ }];
621
+
622
+ mockUseEvents.mockReturnValue({
623
+ ...defaultMockContext,
624
+ events: eventsWithoutDate,
625
+ selectedEvent: eventsWithoutDate[0],
626
+ });
627
+
628
+ renderWithProviders(<EventSelector />);
629
+
630
+ expect(screen.getByTestId('select')).toBeInTheDocument();
631
+ });
632
+
633
+ it('handles events with missing venue gracefully', () => {
634
+ const eventsWithoutVenue: Event[] = [{
635
+ id: 'no-venue-event',
636
+ event_id: 'no-venue-event',
637
+ event_name: 'Event Without Venue',
638
+ event_date: '2024-12-01T00:00:00Z',
639
+ organisation_id: 'org-1',
640
+ created_at: '2024-01-01T00:00:00Z',
641
+ updated_at: '2024-01-01T00:00:00Z',
642
+ }];
643
+
644
+ mockUseEvents.mockReturnValue({
645
+ ...defaultMockContext,
646
+ events: eventsWithoutVenue,
647
+ selectedEvent: eventsWithoutVenue[0],
648
+ });
649
+
650
+ renderWithProviders(<EventSelector />);
651
+
652
+ expect(screen.getByTestId('select')).toBeInTheDocument();
653
+ });
654
+
655
+ it('handles rapid event changes without errors', async () => {
656
+ const user = userEvent.setup();
657
+
658
+ renderWithProviders(<EventSelector />);
659
+
660
+ // Rapidly change events
661
+ const event1 = screen.getByTestId('select-item-event-1');
662
+ const event2 = screen.getByTestId('select-item-event-2');
663
+
664
+ await user.click(event1);
665
+ await user.click(event2);
666
+ await user.click(event1);
667
+
668
+ expect(screen.getByTestId('select')).toBeInTheDocument();
669
+ });
670
+ });
671
+ });
672
+