@jmruthers/pace-core 0.5.118 → 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 (166) 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-7OTQLFVI.js → chunk-B4GZ2BXO.js} +3 -3
  4. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  5. package/dist/chunk-BHWIUEYH.js.map +1 -0
  6. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  7. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  8. package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  9. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  10. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  11. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  12. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  13. package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
  14. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  15. package/dist/components.js +8 -8
  16. package/dist/hooks.js +7 -7
  17. package/dist/index.js +11 -11
  18. package/dist/providers.js +2 -2
  19. package/dist/rbac/index.js +7 -7
  20. package/dist/utils.js +1 -1
  21. package/docs/api/classes/ColumnFactory.md +1 -1
  22. package/docs/api/classes/ErrorBoundary.md +1 -1
  23. package/docs/api/classes/InvalidScopeError.md +1 -1
  24. package/docs/api/classes/MissingUserContextError.md +1 -1
  25. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  26. package/docs/api/classes/PermissionDeniedError.md +1 -1
  27. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  28. package/docs/api/classes/RBACAuditManager.md +1 -1
  29. package/docs/api/classes/RBACCache.md +1 -1
  30. package/docs/api/classes/RBACEngine.md +1 -1
  31. package/docs/api/classes/RBACError.md +1 -1
  32. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  33. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  34. package/docs/api/classes/StorageUtils.md +1 -1
  35. package/docs/api/enums/FileCategory.md +1 -1
  36. package/docs/api/interfaces/AggregateConfig.md +1 -1
  37. package/docs/api/interfaces/ButtonProps.md +1 -1
  38. package/docs/api/interfaces/CardProps.md +1 -1
  39. package/docs/api/interfaces/ColorPalette.md +1 -1
  40. package/docs/api/interfaces/ColorShade.md +1 -1
  41. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  42. package/docs/api/interfaces/DataRecord.md +1 -1
  43. package/docs/api/interfaces/DataTableAction.md +1 -1
  44. package/docs/api/interfaces/DataTableColumn.md +1 -1
  45. package/docs/api/interfaces/DataTableProps.md +1 -1
  46. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  47. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  48. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  49. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  50. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  51. package/docs/api/interfaces/FileMetadata.md +1 -1
  52. package/docs/api/interfaces/FileReference.md +1 -1
  53. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  54. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  55. package/docs/api/interfaces/FileUploadProps.md +1 -1
  56. package/docs/api/interfaces/FooterProps.md +1 -1
  57. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  58. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  59. package/docs/api/interfaces/InputProps.md +1 -1
  60. package/docs/api/interfaces/LabelProps.md +1 -1
  61. package/docs/api/interfaces/LoginFormProps.md +1 -1
  62. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  63. package/docs/api/interfaces/NavigationContextType.md +1 -1
  64. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  65. package/docs/api/interfaces/NavigationItem.md +1 -1
  66. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  67. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  68. package/docs/api/interfaces/Organisation.md +1 -1
  69. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  70. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  71. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  72. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  73. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  74. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  75. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  76. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  77. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  78. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  79. package/docs/api/interfaces/PaletteData.md +1 -1
  80. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  81. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  84. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  88. package/docs/api/interfaces/RBACConfig.md +1 -1
  89. package/docs/api/interfaces/RBACLogger.md +1 -1
  90. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  91. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  93. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  94. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  95. package/docs/api/interfaces/RouteConfig.md +1 -1
  96. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  97. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  98. package/docs/api/interfaces/StorageConfig.md +1 -1
  99. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  100. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  101. package/docs/api/interfaces/StorageListOptions.md +1 -1
  102. package/docs/api/interfaces/StorageListResult.md +1 -1
  103. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  104. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  105. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  106. package/docs/api/interfaces/StyleImport.md +1 -1
  107. package/docs/api/interfaces/SwitchProps.md +1 -1
  108. package/docs/api/interfaces/ToastActionElement.md +1 -1
  109. package/docs/api/interfaces/ToastProps.md +1 -1
  110. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  112. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  119. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  121. package/docs/api/interfaces/UserEventAccess.md +1 -1
  122. package/docs/api/interfaces/UserMenuProps.md +1 -1
  123. package/docs/api/interfaces/UserProfile.md +1 -1
  124. package/docs/api/modules.md +2 -2
  125. package/package.json +1 -1
  126. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  127. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  128. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  129. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  130. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  131. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  132. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  133. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  134. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  135. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  136. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  137. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  138. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  139. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  140. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  141. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  142. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  143. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  144. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  145. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  146. package/src/rbac/audit-enhanced.ts +339 -0
  147. package/src/services/EventService.ts +1 -0
  148. package/src/services/__tests__/AuthService.test.ts +473 -0
  149. package/src/services/__tests__/EventService.test.ts +390 -0
  150. package/src/services/__tests__/InactivityService.test.ts +217 -0
  151. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  152. package/dist/chunk-KA3PSVNV.js.map +0 -1
  153. package/src/components/DataTable/utils/debugTools.ts +0 -609
  154. package/src/rbac/testing/index.tsx +0 -340
  155. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  156. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  157. /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
  158. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  159. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  160. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  161. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  162. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  163. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  164. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  165. /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  166. /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
  });