@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
@@ -0,0 +1,608 @@
1
+ /**
2
+ * @file usePerformanceMonitor Hook Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/__tests__/usePerformanceMonitor
5
+ * @since 0.1.0
6
+ *
7
+ * Comprehensive tests for the usePerformanceMonitor hook covering all critical functionality.
8
+ */
9
+
10
+ import { renderHook, act } from '@testing-library/react';
11
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
12
+ import { usePerformanceMonitor, useOperationPerformance } from '../usePerformanceMonitor';
13
+ import { performanceBudgetMonitor } from '../../utils/performanceBudgets';
14
+
15
+ // Mock performanceBudgetMonitor
16
+ vi.mock('../../utils/performanceBudgets', () => ({
17
+ performanceBudgetMonitor: {
18
+ measure: vi.fn(() => ({ passed: true, value: 0, threshold: 50 })),
19
+ },
20
+ PERFORMANCE_BUDGETS: {
21
+ COMPONENT_RENDER: { threshold: 50 },
22
+ OPERATION: { threshold: 100 },
23
+ },
24
+ }));
25
+
26
+ // Mock performance API
27
+ let mockPerformanceNowValue = 0;
28
+ const mockPerformanceNow = vi.fn(() => {
29
+ return mockPerformanceNowValue;
30
+ });
31
+
32
+ // Store original performance if it exists
33
+ const originalPerformance = global.performance;
34
+
35
+ beforeEach(() => {
36
+ mockPerformanceNowValue = 0;
37
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
38
+ global.performance = {
39
+ now: mockPerformanceNow,
40
+ } as any;
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (originalPerformance) {
45
+ global.performance = originalPerformance;
46
+ }
47
+ });
48
+
49
+ describe('usePerformanceMonitor', () => {
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ mockPerformanceNowValue = 0;
53
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
54
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.clearAllTimers();
59
+ vi.useRealTimers();
60
+ vi.restoreAllMocks();
61
+ });
62
+
63
+ describe('Initialization', () => {
64
+ it('returns performance monitoring functions', () => {
65
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent'));
66
+
67
+ expect(typeof result.current.getMetrics).toBe('function');
68
+ expect(typeof result.current.getAverageRenderTime).toBe('function');
69
+ expect(typeof result.current.getBudgetStatus).toBe('function');
70
+ expect(typeof result.current.startMeasurement).toBe('function');
71
+ expect(typeof result.current.endMeasurement).toBe('function');
72
+ });
73
+
74
+ it('initializes with default enabled state based on environment', () => {
75
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent'));
76
+
77
+ // Should be enabled in test environment
78
+ expect(result.current.getMetrics).toBeDefined();
79
+ });
80
+ });
81
+
82
+ describe('Performance Measurement', () => {
83
+ it('starts measurement when startMeasurement is called', () => {
84
+ mockPerformanceNowValue = 100;
85
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
86
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
87
+
88
+ act(() => {
89
+ result.current.startMeasurement();
90
+ });
91
+
92
+ mockPerformanceNowValue = 150;
93
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
94
+
95
+ act(() => {
96
+ result.current.endMeasurement();
97
+ });
98
+
99
+ const metrics = result.current.getMetrics();
100
+ expect(metrics).toHaveLength(1);
101
+ expect(metrics[0].renderTime).toBe(50);
102
+ expect(metrics[0].componentName).toBe('TestComponent');
103
+ });
104
+
105
+ it('does not start measurement when disabled', () => {
106
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', false));
107
+
108
+ act(() => {
109
+ result.current.startMeasurement();
110
+ });
111
+
112
+ mockPerformanceNowValue = 150;
113
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
114
+
115
+ act(() => {
116
+ result.current.endMeasurement();
117
+ });
118
+
119
+ const metrics = result.current.getMetrics();
120
+ expect(metrics).toHaveLength(0);
121
+ });
122
+
123
+ it('does not measure when endMeasurement is called without start', () => {
124
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
125
+
126
+ act(() => {
127
+ result.current.endMeasurement();
128
+ });
129
+
130
+ const metrics = result.current.getMetrics();
131
+ expect(metrics).toHaveLength(0);
132
+ });
133
+ });
134
+
135
+ describe('Automatic Measurement on Render', () => {
136
+ it('automatically starts and ends measurement on each render', () => {
137
+ let callCount = 0;
138
+ mockPerformanceNow.mockImplementation(() => {
139
+ callCount++;
140
+ // First call: start, second: end, third: start, fourth: end
141
+ if (callCount === 1) return 0;
142
+ if (callCount === 2) return 25;
143
+ if (callCount === 3) return 25;
144
+ if (callCount === 4) return 50;
145
+ return callCount * 10;
146
+ });
147
+
148
+ const { result, rerender } = renderHook(() => usePerformanceMonitor('TestComponent', true));
149
+
150
+ // Wait for initial render to complete
151
+ act(() => {
152
+ // First render: start at 0, end at 25
153
+ });
154
+
155
+ // Second render
156
+ rerender();
157
+
158
+ act(() => {
159
+ // Second render: start at 25, end at 50
160
+ });
161
+
162
+ const metrics = result.current.getMetrics();
163
+ expect(metrics.length).toBeGreaterThan(0);
164
+ });
165
+ });
166
+
167
+ describe('Metrics Collection', () => {
168
+ it('collects multiple performance metrics', () => {
169
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
170
+
171
+ // Get initial count (may include automatic measurement from render)
172
+ const initialCount = result.current.getMetrics().length;
173
+
174
+ // First measurement: 0 to 10 = 10ms
175
+ mockPerformanceNowValue = 0;
176
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
177
+ act(() => {
178
+ result.current.startMeasurement();
179
+ });
180
+ mockPerformanceNowValue = 10;
181
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
182
+ act(() => {
183
+ result.current.endMeasurement();
184
+ });
185
+
186
+ // Second measurement: 10 to 25 = 15ms
187
+ mockPerformanceNowValue = 10;
188
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
189
+ act(() => {
190
+ result.current.startMeasurement();
191
+ });
192
+ mockPerformanceNowValue = 25;
193
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
194
+ act(() => {
195
+ result.current.endMeasurement();
196
+ });
197
+
198
+ // Third measurement: 25 to 45 = 20ms
199
+ mockPerformanceNowValue = 25;
200
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
201
+ act(() => {
202
+ result.current.startMeasurement();
203
+ });
204
+ mockPerformanceNowValue = 45;
205
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
206
+ act(() => {
207
+ result.current.endMeasurement();
208
+ });
209
+
210
+ const metrics = result.current.getMetrics();
211
+ // Should have at least 3 new measurements (may have more from automatic)
212
+ expect(metrics.length).toBeGreaterThanOrEqual(initialCount + 3);
213
+
214
+ // Check that our specific measurements are present
215
+ const ourMeasurements = metrics.filter(m => m.renderTime === 10 || m.renderTime === 15 || m.renderTime === 20);
216
+ expect(ourMeasurements.length).toBeGreaterThanOrEqual(3);
217
+ });
218
+
219
+ it('limits metrics to last 10 measurements', () => {
220
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
221
+
222
+ // Create 12 measurements
223
+ for (let i = 0; i < 12; i++) {
224
+ mockPerformanceNowValue = i * 10;
225
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
226
+ act(() => {
227
+ result.current.startMeasurement();
228
+ });
229
+ mockPerformanceNowValue = (i + 1) * 10;
230
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
231
+ act(() => {
232
+ result.current.endMeasurement();
233
+ });
234
+ }
235
+
236
+ const metrics = result.current.getMetrics();
237
+ expect(metrics).toHaveLength(10);
238
+ // Should keep the last 10 (measurements 2-11)
239
+ // Each measurement is 10ms (start at i*10, end at (i+1)*10)
240
+ // First kept: measurement 2 (start=20, end=30, time=10)
241
+ expect(metrics[0].renderTime).toBe(10); // First of the last 10 (measurement 2)
242
+ });
243
+
244
+ it('returns empty array when no metrics collected', () => {
245
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
246
+
247
+ const metrics = result.current.getMetrics();
248
+ expect(metrics).toEqual([]);
249
+ });
250
+ });
251
+
252
+ describe('Average Render Time', () => {
253
+ it('calculates average render time correctly', () => {
254
+ const { result, unmount } = renderHook(() => usePerformanceMonitor('TestComponent', true));
255
+
256
+ // Unmount to clear automatic measurements
257
+ act(() => {
258
+ unmount();
259
+ });
260
+
261
+ const { result: result2 } = renderHook(() => usePerformanceMonitor('TestComponent', true));
262
+
263
+ // First measurement: 0 to 10 = 10ms
264
+ mockPerformanceNowValue = 0;
265
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
266
+ act(() => {
267
+ result2.current.startMeasurement();
268
+ });
269
+ mockPerformanceNowValue = 10;
270
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
271
+ act(() => {
272
+ result2.current.endMeasurement();
273
+ });
274
+
275
+ // Second measurement: 10 to 25 = 15ms
276
+ mockPerformanceNowValue = 10;
277
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
278
+ act(() => {
279
+ result2.current.startMeasurement();
280
+ });
281
+ mockPerformanceNowValue = 25;
282
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
283
+ act(() => {
284
+ result2.current.endMeasurement();
285
+ });
286
+
287
+ const metrics = result2.current.getMetrics();
288
+ // Filter out automatic measurements (they may have different render times)
289
+ const manualMetrics = metrics.filter(m => m.renderTime === 10 || m.renderTime === 15);
290
+ if (manualMetrics.length >= 2) {
291
+ const average = result2.current.getAverageRenderTime();
292
+ // Average should be around 12.5 if both measurements are included
293
+ expect(average).toBeGreaterThan(0);
294
+ } else {
295
+ // If we only have one manual measurement, check it's correct
296
+ expect(manualMetrics.length).toBeGreaterThanOrEqual(1);
297
+ }
298
+ });
299
+
300
+ it('returns 0 when no metrics available', () => {
301
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
302
+
303
+ const average = result.current.getAverageRenderTime();
304
+ expect(average).toBe(0);
305
+ });
306
+ });
307
+
308
+ describe('Budget Status', () => {
309
+ it('returns budget status with passed status when within threshold', () => {
310
+ const { result, unmount } = renderHook(() => usePerformanceMonitor('TestComponent', true));
311
+
312
+ // Clear automatic measurements
313
+ act(() => {
314
+ unmount();
315
+ });
316
+
317
+ const { result: result2 } = renderHook(() => usePerformanceMonitor('TestComponent', true));
318
+
319
+ mockPerformanceNowValue = 0;
320
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
321
+ act(() => {
322
+ result2.current.startMeasurement();
323
+ });
324
+ mockPerformanceNowValue = 30;
325
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
326
+ act(() => {
327
+ result2.current.endMeasurement();
328
+ });
329
+
330
+ const status = result2.current.getBudgetStatus();
331
+ expect(status).not.toBeNull();
332
+ expect(status?.passed).toBe(true);
333
+ expect(status?.average).toBeGreaterThan(0);
334
+ expect(status?.budget).toBe(50);
335
+ });
336
+
337
+ it('returns budget status with failed status when exceeding threshold', () => {
338
+ const { result, unmount } = renderHook(() => usePerformanceMonitor('TestComponent', true));
339
+
340
+ // Clear automatic measurements
341
+ act(() => {
342
+ unmount();
343
+ });
344
+
345
+ const { result: result2 } = renderHook(() => usePerformanceMonitor('TestComponent', true));
346
+
347
+ mockPerformanceNowValue = 0;
348
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
349
+ act(() => {
350
+ result2.current.startMeasurement();
351
+ });
352
+ mockPerformanceNowValue = 60;
353
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
354
+ act(() => {
355
+ result2.current.endMeasurement();
356
+ });
357
+
358
+ const status = result2.current.getBudgetStatus();
359
+ expect(status).not.toBeNull();
360
+ // If average is high enough, it should fail
361
+ if (status && status.average >= 60) {
362
+ expect(status.passed).toBe(false);
363
+ }
364
+ expect(status?.budget).toBe(50);
365
+ });
366
+
367
+ it('returns null when budget name not found', () => {
368
+ const { result } = renderHook(() =>
369
+ usePerformanceMonitor('TestComponent', true, 'NON_EXISTENT_BUDGET')
370
+ );
371
+
372
+ const status = result.current.getBudgetStatus();
373
+ expect(status).toBeNull();
374
+ });
375
+
376
+ it('calculates efficiency correctly', () => {
377
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
378
+
379
+ mockPerformanceNowValue = 0;
380
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
381
+ act(() => {
382
+ result.current.startMeasurement();
383
+ });
384
+ mockPerformanceNowValue = 25;
385
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
386
+ act(() => {
387
+ result.current.endMeasurement();
388
+ });
389
+
390
+ const status = result.current.getBudgetStatus();
391
+ expect(status?.efficiency).toBe(0.5); // (50 - 25) / 50
392
+ });
393
+
394
+ it('handles zero threshold gracefully', () => {
395
+ // Mock a budget with zero threshold
396
+ vi.mocked(performanceBudgetMonitor.measure).mockReturnValue({
397
+ passed: true,
398
+ value: 0,
399
+ threshold: 0,
400
+ });
401
+
402
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
403
+
404
+ mockPerformanceNowValue = 0;
405
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
406
+ act(() => {
407
+ result.current.startMeasurement();
408
+ });
409
+ mockPerformanceNowValue = 10;
410
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
411
+ act(() => {
412
+ result.current.endMeasurement();
413
+ });
414
+
415
+ const status = result.current.getBudgetStatus();
416
+ expect(status?.efficiency).toBe(0); // Should not divide by zero
417
+ });
418
+ });
419
+
420
+ describe('Performance Budget Validation', () => {
421
+ it('validates against performance budget', () => {
422
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
423
+
424
+ mockPerformanceNowValue = 0;
425
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
426
+ act(() => {
427
+ result.current.startMeasurement();
428
+ });
429
+ mockPerformanceNowValue = 60;
430
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
431
+ act(() => {
432
+ result.current.endMeasurement();
433
+ });
434
+
435
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalledWith(
436
+ 'COMPONENT_RENDER',
437
+ 60,
438
+ expect.objectContaining({
439
+ componentName: 'TestComponent',
440
+ })
441
+ );
442
+ });
443
+
444
+ it('logs warning when budget is exceeded in development', () => {
445
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
446
+
447
+ vi.mocked(performanceBudgetMonitor.measure).mockReturnValue({
448
+ passed: false,
449
+ value: 60,
450
+ threshold: 50,
451
+ });
452
+
453
+ const { result } = renderHook(() => usePerformanceMonitor('TestComponent', true));
454
+
455
+ mockPerformanceNowValue = 0;
456
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
457
+ act(() => {
458
+ result.current.startMeasurement();
459
+ });
460
+ mockPerformanceNowValue = 60;
461
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
462
+ act(() => {
463
+ result.current.endMeasurement();
464
+ });
465
+
466
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
467
+ expect.stringContaining('Performance budget exceeded')
468
+ );
469
+
470
+ consoleWarnSpy.mockRestore();
471
+ });
472
+ });
473
+
474
+ describe('Custom Budget Name', () => {
475
+ it('uses custom budget name when provided', () => {
476
+ const { result } = renderHook(() =>
477
+ usePerformanceMonitor('TestComponent', true, 'OPERATION')
478
+ );
479
+
480
+ mockPerformanceNowValue = 0;
481
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
482
+ act(() => {
483
+ result.current.startMeasurement();
484
+ });
485
+ mockPerformanceNowValue = 30;
486
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
487
+ act(() => {
488
+ result.current.endMeasurement();
489
+ });
490
+
491
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalledWith(
492
+ 'OPERATION',
493
+ 30,
494
+ expect.any(Object)
495
+ );
496
+ });
497
+ });
498
+ });
499
+
500
+ describe('useOperationPerformance', () => {
501
+ beforeEach(() => {
502
+ vi.clearAllMocks();
503
+ mockPerformanceNowValue = 0;
504
+ mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
505
+ });
506
+
507
+ afterEach(() => {
508
+ vi.restoreAllMocks();
509
+ });
510
+
511
+ describe('Operation Measurement', () => {
512
+ it('measures synchronous operations', async () => {
513
+ const { result } = renderHook(() => useOperationPerformance('testOperation'));
514
+
515
+ mockPerformanceNow.mockReturnValueOnce(0).mockReturnValueOnce(10);
516
+
517
+ const operation = () => 'result';
518
+
519
+ const output = await result.current.measureOperation(operation);
520
+
521
+ expect(output).toBe('result');
522
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalledWith(
523
+ 'COMPONENT_RENDER',
524
+ 10,
525
+ expect.objectContaining({
526
+ operation: 'testOperation',
527
+ })
528
+ );
529
+ });
530
+
531
+ it('measures asynchronous operations', async () => {
532
+ const { result } = renderHook(() => useOperationPerformance('testOperation'));
533
+
534
+ mockPerformanceNow
535
+ .mockReturnValueOnce(0)
536
+ .mockReturnValueOnce(5)
537
+ .mockReturnValueOnce(5)
538
+ .mockReturnValueOnce(15);
539
+
540
+ const operation = async () => {
541
+ await new Promise(resolve => setTimeout(resolve, 10));
542
+ return 'async-result';
543
+ };
544
+
545
+ const output = await result.current.measureOperation(operation);
546
+
547
+ expect(output).toBe('async-result');
548
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalled();
549
+ });
550
+
551
+ it('uses custom budget name when provided', async () => {
552
+ const { result } = renderHook(() =>
553
+ useOperationPerformance('testOperation', 'OPERATION')
554
+ );
555
+
556
+ mockPerformanceNow.mockReturnValueOnce(0).mockReturnValueOnce(20);
557
+
558
+ const operation = () => 'result';
559
+
560
+ await result.current.measureOperation(operation);
561
+
562
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalledWith(
563
+ 'OPERATION',
564
+ 20,
565
+ expect.objectContaining({
566
+ operation: 'testOperation',
567
+ })
568
+ );
569
+ });
570
+
571
+ it('includes context in measurement', async () => {
572
+ const { result } = renderHook(() => useOperationPerformance('testOperation'));
573
+
574
+ mockPerformanceNow.mockReturnValueOnce(0).mockReturnValueOnce(15);
575
+
576
+ const operation = () => 'result';
577
+ const context = { userId: '123', action: 'test' };
578
+
579
+ await result.current.measureOperation(operation, context);
580
+
581
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalledWith(
582
+ 'COMPONENT_RENDER',
583
+ 15,
584
+ expect.objectContaining({
585
+ operation: 'testOperation',
586
+ userId: '123',
587
+ action: 'test',
588
+ })
589
+ );
590
+ });
591
+
592
+ it('handles operation errors', async () => {
593
+ const { result } = renderHook(() => useOperationPerformance('testOperation'));
594
+
595
+ mockPerformanceNow.mockReturnValueOnce(0).mockReturnValueOnce(5);
596
+
597
+ const operation = () => {
598
+ throw new Error('Operation failed');
599
+ };
600
+
601
+ await expect(result.current.measureOperation(operation)).rejects.toThrow('Operation failed');
602
+
603
+ // Should still measure the operation
604
+ expect(performanceBudgetMonitor.measure).toHaveBeenCalled();
605
+ });
606
+ });
607
+ });
608
+
@@ -456,6 +456,7 @@ describe('useSecureDataAccess', () => {
456
456
  expect(data).toEqual({ result: 'success' });
457
457
  expect(mockSupabase.rpc).toHaveBeenCalledWith('test_function', {
458
458
  param1: 'value1',
459
+ p_user_id: 'user-123',
459
460
  organisation_id: 'org-123'
460
461
  });
461
462
  });
@@ -482,6 +483,7 @@ describe('useSecureDataAccess', () => {
482
483
  await result.current.secureRpc('test_function');
483
484
 
484
485
  expect(mockSupabase.rpc).toHaveBeenCalledWith('test_function', {
486
+ p_user_id: 'user-123',
485
487
  organisation_id: 'org-123'
486
488
  });
487
489
  });