@jmruthers/pace-core 0.5.117 → 0.5.119

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 (167) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
  2. package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
  3. package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
  4. package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
  5. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  6. package/dist/chunk-BHWIUEYH.js.map +1 -0
  7. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  8. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  9. package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  10. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  11. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  12. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  13. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  14. package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
  15. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  16. package/dist/components.js +8 -8
  17. package/dist/hooks.js +7 -7
  18. package/dist/index.js +11 -11
  19. package/dist/providers.js +2 -2
  20. package/dist/rbac/index.js +7 -7
  21. package/dist/utils.js +1 -1
  22. package/docs/api/classes/ColumnFactory.md +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/classes/StorageUtils.md +1 -1
  36. package/docs/api/enums/FileCategory.md +1 -1
  37. package/docs/api/interfaces/AggregateConfig.md +1 -1
  38. package/docs/api/interfaces/ButtonProps.md +1 -1
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  51. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  52. package/docs/api/interfaces/FileMetadata.md +1 -1
  53. package/docs/api/interfaces/FileReference.md +1 -1
  54. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  55. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  56. package/docs/api/interfaces/FileUploadProps.md +1 -1
  57. package/docs/api/interfaces/FooterProps.md +1 -1
  58. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  59. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  60. package/docs/api/interfaces/InputProps.md +1 -1
  61. package/docs/api/interfaces/LabelProps.md +1 -1
  62. package/docs/api/interfaces/LoginFormProps.md +1 -1
  63. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  64. package/docs/api/interfaces/NavigationContextType.md +1 -1
  65. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  66. package/docs/api/interfaces/NavigationItem.md +1 -1
  67. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  68. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  69. package/docs/api/interfaces/Organisation.md +1 -1
  70. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  71. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  72. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  73. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  74. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  75. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  76. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  77. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  78. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  79. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  80. package/docs/api/interfaces/PaletteData.md +1 -1
  81. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  82. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  84. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  85. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  88. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  89. package/docs/api/interfaces/RBACConfig.md +1 -1
  90. package/docs/api/interfaces/RBACLogger.md +1 -1
  91. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  93. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  94. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  95. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  96. package/docs/api/interfaces/RouteConfig.md +1 -1
  97. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  98. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  99. package/docs/api/interfaces/StorageConfig.md +1 -1
  100. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  101. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  102. package/docs/api/interfaces/StorageListOptions.md +1 -1
  103. package/docs/api/interfaces/StorageListResult.md +1 -1
  104. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  105. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  106. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  107. package/docs/api/interfaces/StyleImport.md +1 -1
  108. package/docs/api/interfaces/SwitchProps.md +1 -1
  109. package/docs/api/interfaces/ToastActionElement.md +1 -1
  110. package/docs/api/interfaces/ToastProps.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  112. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  114. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  116. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  118. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  119. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  121. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  122. package/docs/api/interfaces/UserEventAccess.md +1 -1
  123. package/docs/api/interfaces/UserMenuProps.md +1 -1
  124. package/docs/api/interfaces/UserProfile.md +1 -1
  125. package/docs/api/modules.md +2 -2
  126. package/package.json +1 -1
  127. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  128. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  129. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  130. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  131. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  132. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  133. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  134. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  135. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  136. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  137. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  138. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  139. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  140. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  141. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  142. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  143. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  144. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  145. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  146. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  147. package/src/hooks/useSecureDataAccess.ts +43 -5
  148. package/src/rbac/audit-enhanced.ts +339 -0
  149. package/src/services/EventService.ts +1 -0
  150. package/src/services/__tests__/AuthService.test.ts +473 -0
  151. package/src/services/__tests__/EventService.test.ts +390 -0
  152. package/src/services/__tests__/InactivityService.test.ts +217 -0
  153. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  154. package/dist/chunk-KA3PSVNV.js.map +0 -1
  155. package/src/components/DataTable/utils/debugTools.ts +0 -609
  156. package/src/rbac/testing/index.tsx +0 -340
  157. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  158. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  159. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  160. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  161. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  162. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  163. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  164. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  165. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  166. /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  167. /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
@@ -1,62 +1,463 @@
1
+ /**
2
+ * @file useToast Hook Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/__tests__/useToast
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for the useToast hook covering all critical functionality.
8
+ */
1
9
 
2
- import { renderHook, act } from '@testing-library/react';
3
- import { describe, it, expect, beforeEach } from 'vitest';
4
- import { useToast, reset } from '../useToast';
10
+ import { renderHook, act, waitFor } from '@testing-library/react';
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import { useToast, reset, toast } from '../useToast';
13
+
14
+ const TOAST_REMOVE_DELAY = 1000;
15
+ const DEFAULT_TOAST_DURATION = 10000;
5
16
 
6
17
  describe('useToast', () => {
7
18
  beforeEach(() => {
19
+ vi.useFakeTimers();
8
20
  reset();
9
21
  });
10
22
 
11
- it('should add a toast', () => {
12
- const { result } = renderHook(() => useToast());
23
+ afterEach(() => {
24
+ vi.clearAllTimers();
25
+ vi.useRealTimers();
26
+ });
27
+
28
+ describe('Toast Creation', () => {
29
+ it('adds a toast with title and description', () => {
30
+ const { result } = renderHook(() => useToast());
13
31
 
14
- act(() => {
15
- result.current.toast({
32
+ act(() => {
33
+ result.current.toast({
34
+ title: 'Test Toast',
35
+ description: 'This is a test toast',
36
+ });
37
+ });
38
+
39
+ expect(result.current.toasts).toHaveLength(1);
40
+ expect(result.current.toasts[0]).toMatchObject({
16
41
  title: 'Test Toast',
17
42
  description: 'This is a test toast',
43
+ open: true,
44
+ });
45
+ });
46
+
47
+ it('adds a toast with default duration', () => {
48
+ const { result } = renderHook(() => useToast());
49
+
50
+ act(() => {
51
+ result.current.toast({
52
+ title: 'Test Toast',
53
+ });
54
+ });
55
+
56
+ expect(result.current.toasts[0].duration).toBe(DEFAULT_TOAST_DURATION);
57
+ });
58
+
59
+ it('ignores custom duration and uses default', () => {
60
+ const { result } = renderHook(() => useToast());
61
+
62
+ act(() => {
63
+ // Even if duration is provided, it should be ignored
64
+ result.current.toast({
65
+ title: 'Test Toast',
66
+ // @ts-expect-error - duration should not be in props
67
+ duration: 5000,
68
+ });
69
+ });
70
+
71
+ expect(result.current.toasts[0].duration).toBe(DEFAULT_TOAST_DURATION);
72
+ });
73
+
74
+ it('adds toast with variant', () => {
75
+ const { result } = renderHook(() => useToast());
76
+
77
+ act(() => {
78
+ result.current.toast({
79
+ title: 'Success Toast',
80
+ variant: 'success',
81
+ });
18
82
  });
83
+
84
+ expect(result.current.toasts[0].variant).toBe('success');
19
85
  });
20
86
 
21
- expect(result.current.toasts).toHaveLength(1);
22
- expect(result.current.toasts[0]).toMatchObject({
23
- title: 'Test Toast',
24
- description: 'This is a test toast',
87
+ it('adds toast with action', () => {
88
+ const { result } = renderHook(() => useToast());
89
+ const actionButton = <button>Action</button>;
90
+
91
+ act(() => {
92
+ result.current.toast({
93
+ title: 'Test Toast',
94
+ action: actionButton,
95
+ });
96
+ });
97
+
98
+ expect(result.current.toasts[0].action).toBe(actionButton);
25
99
  });
26
100
  });
27
101
 
28
- it('should dismiss a toast', () => {
29
- const { result } = renderHook(() => useToast());
102
+ describe('Toast Dismissal', () => {
103
+ it('dismisses a toast using dismiss function', () => {
104
+ const { result } = renderHook(() => useToast());
30
105
 
31
- let dismissFn: (() => void) | undefined;
106
+ let dismissFn: (() => void) | undefined;
32
107
 
33
- act(() => {
34
- const response = result.current.toast({
35
- title: 'Test Toast',
108
+ act(() => {
109
+ const response = result.current.toast({
110
+ title: 'Test Toast',
111
+ });
112
+ dismissFn = response.dismiss;
113
+ });
114
+
115
+ expect(result.current.toasts).toHaveLength(1);
116
+ expect(result.current.toasts[0].open).toBe(true);
117
+
118
+ act(() => {
119
+ dismissFn?.();
36
120
  });
37
- dismissFn = response.dismiss;
121
+
122
+ expect(result.current.toasts[0].open).toBe(false);
38
123
  });
39
124
 
40
- expect(result.current.toasts).toHaveLength(1);
125
+ it('dismisses a toast using dismiss method with toast ID', () => {
126
+ const { result } = renderHook(() => useToast());
127
+
128
+ let toastId: string | undefined;
41
129
 
42
- act(() => {
43
- dismissFn?.();
130
+ act(() => {
131
+ const response = result.current.toast({
132
+ title: 'Test Toast',
133
+ });
134
+ toastId = response.id;
135
+ });
136
+
137
+ expect(result.current.toasts[0].open).toBe(true);
138
+
139
+ act(() => {
140
+ result.current.dismiss(toastId);
141
+ });
142
+
143
+ expect(result.current.toasts[0].open).toBe(false);
44
144
  });
45
145
 
46
- expect(result.current.toasts[0].open).toBe(false);
47
- });
146
+ it('dismisses all toasts when no ID provided', () => {
147
+ const { result } = renderHook(() => useToast());
48
148
 
49
- it('should limit the number of toasts', () => {
50
- const { result } = renderHook(() => useToast());
149
+ act(() => {
150
+ result.current.toast({ title: 'Toast 1' });
151
+ result.current.toast({ title: 'Toast 2' });
152
+ result.current.toast({ title: 'Toast 3' });
153
+ });
154
+
155
+ expect(result.current.toasts).toHaveLength(3);
156
+ expect(result.current.toasts.every(t => t.open)).toBe(true);
157
+
158
+ act(() => {
159
+ result.current.dismiss();
160
+ });
161
+
162
+ expect(result.current.toasts.every(t => !t.open)).toBe(true);
163
+ });
51
164
 
52
- act(() => {
53
- for (let i = 0; i < 10; i++) {
165
+ it('calls onOpenChange handler when open changes', () => {
166
+ const { result } = renderHook(() => useToast());
167
+ const onOpenChange = vi.fn();
168
+
169
+ act(() => {
54
170
  result.current.toast({
55
- title: `Toast ${i}`,
171
+ title: 'Test Toast',
172
+ onOpenChange,
173
+ });
174
+ });
175
+
176
+ act(() => {
177
+ result.current.toasts[0].onOpenChange?.(false);
178
+ });
179
+
180
+ expect(onOpenChange).toHaveBeenCalledWith(false);
181
+ expect(result.current.toasts[0].open).toBe(false);
182
+ });
183
+
184
+ it('removes toast after delay when dismissed', async () => {
185
+ const { result } = renderHook(() => useToast());
186
+
187
+ let toastId: string | undefined;
188
+
189
+ act(() => {
190
+ const response = result.current.toast({
191
+ title: 'Test Toast',
192
+ });
193
+ toastId = response.id;
194
+ });
195
+
196
+ expect(result.current.toasts).toHaveLength(1);
197
+
198
+ act(() => {
199
+ result.current.dismiss(toastId);
200
+ });
201
+
202
+ expect(result.current.toasts[0].open).toBe(false);
203
+ expect(result.current.toasts).toHaveLength(1); // Still in array
204
+
205
+ // Advance time past removal delay
206
+ act(() => {
207
+ vi.advanceTimersByTime(TOAST_REMOVE_DELAY);
208
+ });
209
+
210
+ await waitFor(() => {
211
+ expect(result.current.toasts).toHaveLength(0);
212
+ }, { timeout: 100 });
213
+ });
214
+
215
+ it('does not remove toast if already in remove queue', () => {
216
+ const { result } = renderHook(() => useToast());
217
+
218
+ let toastId: string | undefined;
219
+
220
+ act(() => {
221
+ const response = result.current.toast({
222
+ title: 'Test Toast',
223
+ });
224
+ toastId = response.id;
225
+ });
226
+
227
+ act(() => {
228
+ result.current.dismiss(toastId);
229
+ });
230
+
231
+ // Try to dismiss again
232
+ act(() => {
233
+ result.current.dismiss(toastId);
234
+ });
235
+
236
+ // Should still only have one toast
237
+ expect(result.current.toasts).toHaveLength(1);
238
+ });
239
+ });
240
+
241
+ describe('Toast Updates', () => {
242
+ it('updates a toast with new properties', () => {
243
+ const { result } = renderHook(() => useToast());
244
+
245
+ let toastId: string | undefined;
246
+ let updateFn: ((props: any) => void) | undefined;
247
+
248
+ act(() => {
249
+ const response = result.current.toast({
250
+ title: 'Original Title',
251
+ description: 'Original Description',
252
+ });
253
+ toastId = response.id;
254
+ updateFn = response.update;
255
+ });
256
+
257
+ expect(result.current.toasts[0].title).toBe('Original Title');
258
+ expect(result.current.toasts[0].description).toBe('Original Description');
259
+
260
+ act(() => {
261
+ updateFn?.({
262
+ title: 'Updated Title',
263
+ description: 'Updated Description',
264
+ });
265
+ });
266
+
267
+ expect(result.current.toasts[0].title).toBe('Updated Title');
268
+ expect(result.current.toasts[0].description).toBe('Updated Description');
269
+ expect(result.current.toasts[0].id).toBe(toastId);
270
+ expect(result.current.toasts[0].duration).toBe(DEFAULT_TOAST_DURATION);
271
+ });
272
+
273
+ it('updates toast variant', () => {
274
+ const { result } = renderHook(() => useToast());
275
+
276
+ let updateFn: ((props: any) => void) | undefined;
277
+
278
+ act(() => {
279
+ const response = result.current.toast({
280
+ title: 'Test Toast',
281
+ variant: 'default',
282
+ });
283
+ updateFn = response.update;
284
+ });
285
+
286
+ expect(result.current.toasts[0].variant).toBe('default');
287
+
288
+ act(() => {
289
+ updateFn?.({
290
+ variant: 'destructive',
291
+ });
292
+ });
293
+
294
+ expect(result.current.toasts[0].variant).toBe('destructive');
295
+ });
296
+ });
297
+
298
+ describe('Toast Limits', () => {
299
+ it('limits toasts to maximum of 5', () => {
300
+ const { result } = renderHook(() => useToast());
301
+
302
+ act(() => {
303
+ for (let i = 0; i < 10; i++) {
304
+ result.current.toast({
305
+ title: `Toast ${i}`,
306
+ });
307
+ }
308
+ });
309
+
310
+ expect(result.current.toasts.length).toBeLessThanOrEqual(5);
311
+ expect(result.current.toasts.length).toBe(5);
312
+ });
313
+
314
+ it('keeps newest toasts when limit exceeded', () => {
315
+ const { result } = renderHook(() => useToast());
316
+
317
+ act(() => {
318
+ for (let i = 0; i < 7; i++) {
319
+ result.current.toast({
320
+ title: `Toast ${i}`,
321
+ });
322
+ }
323
+ });
324
+
325
+ expect(result.current.toasts.length).toBe(5);
326
+ // Should keep the last 5 (toasts 2-6)
327
+ expect(result.current.toasts[0].title).toBe('Toast 6');
328
+ expect(result.current.toasts[4].title).toBe('Toast 2');
329
+ });
330
+ });
331
+
332
+ describe('Direct Toast Function', () => {
333
+ it('creates toast using direct toast function', () => {
334
+ const { result } = renderHook(() => useToast());
335
+
336
+ act(() => {
337
+ toast({
338
+ title: 'Direct Toast',
56
339
  });
57
- }
340
+ });
341
+
342
+ expect(result.current.toasts).toHaveLength(1);
343
+ expect(result.current.toasts[0].title).toBe('Direct Toast');
58
344
  });
345
+ });
346
+
347
+ describe('Reset Function', () => {
348
+ it('resets all toasts and clears timeouts', () => {
349
+ const { result } = renderHook(() => useToast());
59
350
 
60
- expect(result.current.toasts.length).toBeLessThanOrEqual(5);
351
+ act(() => {
352
+ result.current.toast({ title: 'Toast 1' });
353
+ result.current.toast({ title: 'Toast 2' });
354
+ });
355
+
356
+ expect(result.current.toasts).toHaveLength(2);
357
+
358
+ act(() => {
359
+ reset();
360
+ });
361
+
362
+ // Create new hook instance to verify reset cleared global state
363
+ const { result: result2 } = renderHook(() => useToast());
364
+ expect(result2.current.toasts).toHaveLength(0);
365
+ });
366
+ });
367
+
368
+ describe('Multiple Hook Instances', () => {
369
+ it('shares state between multiple hook instances', () => {
370
+ const { result: result1 } = renderHook(() => useToast());
371
+ const { result: result2 } = renderHook(() => useToast());
372
+
373
+ act(() => {
374
+ result1.current.toast({
375
+ title: 'Toast from instance 1',
376
+ });
377
+ });
378
+
379
+ expect(result1.current.toasts).toHaveLength(1);
380
+ expect(result2.current.toasts).toHaveLength(1);
381
+ expect(result2.current.toasts[0].title).toBe('Toast from instance 1');
382
+ });
383
+ });
384
+
385
+ describe('Remove Queue', () => {
386
+ it('removes toast by ID after delay', async () => {
387
+ const { result } = renderHook(() => useToast());
388
+
389
+ let toastId: string | undefined;
390
+
391
+ act(() => {
392
+ const response = result.current.toast({
393
+ title: 'Test Toast',
394
+ });
395
+ toastId = response.id;
396
+ });
397
+
398
+ expect(result.current.toasts).toHaveLength(1);
399
+
400
+ act(() => {
401
+ result.current.dismiss(toastId);
402
+ });
403
+
404
+ expect(result.current.toasts[0].open).toBe(false);
405
+ expect(result.current.toasts).toHaveLength(1); // Still in array
406
+
407
+ // Advance time past removal delay - this should trigger the setTimeout callback
408
+ act(() => {
409
+ vi.advanceTimersByTime(TOAST_REMOVE_DELAY);
410
+ });
411
+
412
+ // The setTimeout callback should have fired and dispatched REMOVE_TOAST
413
+ // Wait for React to process the state update
414
+ await waitFor(() => {
415
+ expect(result.current.toasts).toHaveLength(0);
416
+ }, { timeout: 200 });
417
+ });
418
+
419
+ it('dismisses all toasts when toastId is undefined', async () => {
420
+ const { result } = renderHook(() => useToast());
421
+
422
+ let toastId1: string | undefined;
423
+ let toastId2: string | undefined;
424
+
425
+ act(() => {
426
+ const response1 = result.current.toast({ title: 'Toast 1' });
427
+ const response2 = result.current.toast({ title: 'Toast 2' });
428
+ toastId1 = response1.id;
429
+ toastId2 = response2.id;
430
+ });
431
+
432
+ expect(result.current.toasts).toHaveLength(2);
433
+
434
+ // Dismiss all - this sets all toasts to open: false but doesn't add them to remove queue
435
+ act(() => {
436
+ result.current.dismiss(); // Dismiss all
437
+ });
438
+
439
+ expect(result.current.toasts.every(t => !t.open)).toBe(true);
440
+ expect(result.current.toasts).toHaveLength(2); // Still in array (not removed yet)
441
+
442
+ // Note: When dismissing all (toastId undefined), individual toasts are NOT added to remove queue
443
+ // They remain in the array but are closed. To actually remove them, we need to dismiss individually
444
+ // or wait for their auto-dismiss timers (if they have them)
445
+
446
+ // Test that dismissing individually removes them
447
+ act(() => {
448
+ result.current.dismiss(toastId1);
449
+ result.current.dismiss(toastId2);
450
+ });
451
+
452
+ // Advance time past removal delay
453
+ act(() => {
454
+ vi.advanceTimersByTime(TOAST_REMOVE_DELAY);
455
+ });
456
+
457
+ // Wait for state update
458
+ await waitFor(() => {
459
+ expect(result.current.toasts).toHaveLength(0);
460
+ }, { timeout: 200 });
461
+ });
61
462
  });
62
463
  });
@@ -303,6 +303,7 @@ describe('useSecureDataAccess', () => {
303
303
 
304
304
  expect(freshMockSupabase.rpc).toHaveBeenCalledWith('get_user_data', {
305
305
  user_id: 'user-123',
306
+ p_user_id: 'user-123',
306
307
  organisation_id: 'org-123'
307
308
  });
308
309
  });
@@ -507,15 +507,53 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
507
507
  'app_cake_delivery_upsert'
508
508
  ];
509
509
 
510
- const secureParams = {
511
- ...params,
512
- [paramName]: organisationId
513
- };
510
+ // Build secureParams with correct parameter order
511
+ // For functions that require p_event_id as first parameter, ensure it's first
512
+ const secureParams: Record<string, any> = {};
513
+
514
+ // Functions where p_event_id is the FIRST required parameter (no default)
515
+ const functionsWithEventIdFirst = [
516
+ 'data_cake_meals_list',
517
+ 'data_cake_units_list'
518
+ ];
519
+
520
+ // Add p_user_id explicitly for functions that need it (even though it has a default)
521
+ // This ensures parameter matching works correctly
522
+ if (user?.id) {
523
+ secureParams.p_user_id = user.id;
524
+ }
525
+
526
+ // Add organisation_id parameter
527
+ secureParams[paramName] = organisationId;
514
528
 
515
529
  // Add p_event_id if function needs it and event is selected
530
+ // CRITICAL: This must be added AFTER organisation_id but BEFORE caller params
531
+ // to ensure it's not overwritten. For data_cake_items_list, p_event_id is the 3rd param.
516
532
  if (functionsNeedingEventId.includes(functionName) && selectedEvent?.event_id) {
517
533
  secureParams.p_event_id = selectedEvent.event_id;
518
534
  }
535
+
536
+ // Add any other params passed by caller (limit, offset, etc.)
537
+ // NOTE: This will NOT overwrite p_event_id if caller passes it, but we want to ensure
538
+ // our value takes precedence if event is selected
539
+ Object.assign(secureParams, params);
540
+
541
+ // Ensure p_event_id is set if needed (after Object.assign, so it overrides caller params)
542
+ if (functionsNeedingEventId.includes(functionName) && selectedEvent?.event_id) {
543
+ secureParams.p_event_id = selectedEvent.event_id;
544
+ }
545
+
546
+ // Debug logging for items list to help diagnose issues
547
+ if (functionName === 'data_cake_items_list') {
548
+ console.log('[useSecureDataAccess] Calling data_cake_items_list with params:', {
549
+ p_user_id: secureParams.p_user_id,
550
+ p_organisation_id: secureParams.organisation_id || secureParams.p_organisation_id,
551
+ p_event_id: secureParams.p_event_id,
552
+ hasEvent: !!selectedEvent?.event_id,
553
+ eventId: selectedEvent?.event_id,
554
+ allParams: secureParams
555
+ });
556
+ }
519
557
 
520
558
  const { data, error } = await supabase!.rpc(functionName, secureParams);
521
559
 
@@ -529,7 +567,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
529
567
  });
530
568
 
531
569
  return data as T;
532
- }, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, selectedEvent?.event_id]);
570
+ }, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, selectedEvent?.event_id, user?.id]);
533
571
 
534
572
  // NEW: Phase 1 - Enhanced Security Features
535
573
  const [dataAccessHistory, setDataAccessHistory] = useState<DataAccessRecord[]>([]);