@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.
- package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
- package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
- package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
- package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
- package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
- package/dist/chunk-BHWIUEYH.js.map +1 -0
- package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
- package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
- package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
- package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
- package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
- package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
- package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
- package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
- package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
- package/dist/components.js +8 -8
- package/dist/hooks.js +7 -7
- package/dist/index.js +11 -11
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2 -2
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
- package/src/hooks/__tests__/index.unit.test.ts +223 -0
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
- package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
- package/src/hooks/useSecureDataAccess.test.ts +1 -0
- package/src/hooks/useSecureDataAccess.ts +43 -5
- package/src/rbac/audit-enhanced.ts +339 -0
- package/src/services/EventService.ts +1 -0
- package/src/services/__tests__/AuthService.test.ts +473 -0
- package/src/services/__tests__/EventService.test.ts +390 -0
- package/src/services/__tests__/InactivityService.test.ts +217 -0
- package/src/services/__tests__/OrganisationService.test.ts +371 -0
- package/dist/chunk-KA3PSVNV.js.map +0 -1
- package/src/components/DataTable/utils/debugTools.ts +0 -609
- package/src/rbac/testing/index.tsx +0 -340
- /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
- /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
- /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
- /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
- /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
- /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
- /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
- /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
- /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
- /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
|
});
|