@jmruthers/pace-core 0.5.3 → 0.5.5
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-ZQDRE46Q.js → DataTable-3SSI644S.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
- package/dist/chunk-2BJFM2JC.js.map +1 -0
- package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/styles/core.css +3 -0
- package/dist/utils.js +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/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/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +34 -34
- 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/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.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/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/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.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/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/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.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 +3 -3
- package/docs/implementation-guides/data-tables.md +20 -0
- package/docs/quick-reference.md +9 -0
- package/docs/rbac/examples.md +4 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/test-utils.tsx +147 -1
- package/src/components/DataTable/DataTable.tsx +20 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
- package/src/components/DataTable/components/DataTableCore.tsx +167 -138
- package/src/components/Header/Header.test.tsx +1 -1
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
- package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
- package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
- package/src/rbac/api.test.ts +511 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/useCan.test.ts +1 -1
- package/src/rbac/hooks/usePermissions.test.ts +10 -5
- package/src/rbac/hooks/useRBAC.test.ts +141 -93
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/styles/core.css +3 -0
- package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
- package/src/utils/__tests__/audit.unit.test.ts +69 -0
- package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
- package/src/utils/__tests__/cn.unit.test.ts +34 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
- package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
- package/src/utils/__tests__/formatting.unit.test.ts +66 -0
- package/src/utils/__tests__/index.unit.test.ts +251 -0
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
- package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
- package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
- package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
- package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
- package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
- package/src/utils/__tests__/security.unit.test.ts +127 -0
- package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
- package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
- package/src/utils/__tests__/validation.unit.test.ts +84 -0
- package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
- package/src/validation/__tests__/common.unit.test.ts +101 -0
- package/src/validation/__tests__/csrf.unit.test.ts +302 -0
- package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
- package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
- package/dist/chunk-M4RW7PIP.js.map +0 -1
- /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-3SSI644S.js.map} +0 -0
- /package/dist/{chunk-5H3C2SWM.js.map → chunk-RTCA5ZNK.js.map} +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { useComponentPerformance } from '../useComponentPerformance';
|
|
4
|
+
|
|
5
|
+
// Mock performance API
|
|
6
|
+
const mockPerformance = {
|
|
7
|
+
now: vi.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(window, 'performance', {
|
|
11
|
+
value: mockPerformance,
|
|
12
|
+
writable: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Mock console.warn
|
|
16
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
17
|
+
|
|
18
|
+
describe('useComponentPerformance Hook', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockPerformance.now.mockReturnValue(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
consoleSpy.mockRestore();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return component performance data', () => {
|
|
29
|
+
const { result } = renderHook(() => useComponentPerformance({
|
|
30
|
+
componentName: 'TestComponent'
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
expect(result.current.renderCount).toBeDefined();
|
|
34
|
+
expect(result.current.componentName).toBe('TestComponent');
|
|
35
|
+
expect(typeof result.current.renderCount).toBe('number');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should track render count internally', () => {
|
|
39
|
+
const { result, rerender } = renderHook(() => useComponentPerformance({
|
|
40
|
+
componentName: 'TestComponent'
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
expect(result.current.renderCount).toBe(0);
|
|
44
|
+
|
|
45
|
+
// Trigger re-render - renderCount is tracked internally but the returned object doesn't update
|
|
46
|
+
rerender();
|
|
47
|
+
// The render count is tracked internally via useRef, but the returned value doesn't change
|
|
48
|
+
// This is expected behavior since useRef doesn't trigger re-renders
|
|
49
|
+
expect(result.current.componentName).toBe('TestComponent');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should log performance warnings when threshold is exceeded', () => {
|
|
53
|
+
// Set up the spy before any hooks are called
|
|
54
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
55
|
+
|
|
56
|
+
mockPerformance.now
|
|
57
|
+
.mockReturnValueOnce(0) // First render
|
|
58
|
+
.mockReturnValueOnce(50); // Second render (50ms later, exceeds 16ms threshold)
|
|
59
|
+
|
|
60
|
+
const { rerender } = renderHook(() => useComponentPerformance({
|
|
61
|
+
componentName: 'TestComponent',
|
|
62
|
+
threshold: 16,
|
|
63
|
+
enableLogging: true
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// First render
|
|
67
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
// Second render (slow)
|
|
70
|
+
rerender();
|
|
71
|
+
|
|
72
|
+
// The warning should be logged on the second render
|
|
73
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
74
|
+
expect.stringContaining('Performance warning: TestComponent rendered in 50.00ms')
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
consoleSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not log warnings when performance is good', () => {
|
|
81
|
+
mockPerformance.now
|
|
82
|
+
.mockReturnValueOnce(0) // First render
|
|
83
|
+
.mockReturnValueOnce(10); // Second render (10ms later, under threshold)
|
|
84
|
+
|
|
85
|
+
const { rerender } = renderHook(() => useComponentPerformance({
|
|
86
|
+
componentName: 'TestComponent',
|
|
87
|
+
threshold: 16
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// First render
|
|
91
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
92
|
+
|
|
93
|
+
// Second render (fast)
|
|
94
|
+
rerender();
|
|
95
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should respect custom threshold', () => {
|
|
99
|
+
mockPerformance.now
|
|
100
|
+
.mockReturnValueOnce(0) // First render
|
|
101
|
+
.mockReturnValueOnce(25); // Second render (25ms later)
|
|
102
|
+
|
|
103
|
+
const { rerender } = renderHook(() => useComponentPerformance({
|
|
104
|
+
componentName: 'TestComponent',
|
|
105
|
+
threshold: 30 // Custom threshold
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
// First render
|
|
109
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
// Second render (under custom threshold)
|
|
112
|
+
rerender();
|
|
113
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should disable logging when enableLogging is false', () => {
|
|
117
|
+
mockPerformance.now
|
|
118
|
+
.mockReturnValueOnce(0) // First render
|
|
119
|
+
.mockReturnValueOnce(50); // Second render (slow)
|
|
120
|
+
|
|
121
|
+
const { rerender } = renderHook(() => useComponentPerformance({
|
|
122
|
+
componentName: 'TestComponent',
|
|
123
|
+
enableLogging: false
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// First render
|
|
127
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
128
|
+
|
|
129
|
+
// Second render (slow, but logging disabled)
|
|
130
|
+
rerender();
|
|
131
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { useDebounce } from '../useDebounce';
|
|
4
|
+
|
|
5
|
+
describe('useDebounce Hook', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return initial value immediately', () => {
|
|
15
|
+
const { result } = renderHook(() => useDebounce('initial', 500));
|
|
16
|
+
expect(result.current).toBe('initial');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should debounce value changes', () => {
|
|
20
|
+
const { result, rerender } = renderHook(
|
|
21
|
+
({ value }) => useDebounce(value, 500),
|
|
22
|
+
{ initialProps: { value: 'initial' } }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Change value
|
|
26
|
+
rerender({ value: 'changed' });
|
|
27
|
+
expect(result.current).toBe('initial'); // Should still be initial
|
|
28
|
+
|
|
29
|
+
// Fast forward time
|
|
30
|
+
act(() => {
|
|
31
|
+
vi.advanceTimersByTime(500);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result.current).toBe('changed'); // Should now be changed
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should reset timer on rapid changes', () => {
|
|
38
|
+
const { result, rerender } = renderHook(
|
|
39
|
+
({ value }) => useDebounce(value, 500),
|
|
40
|
+
{ initialProps: { value: 'initial' } }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Rapid changes
|
|
44
|
+
rerender({ value: 'change1' });
|
|
45
|
+
act(() => {
|
|
46
|
+
vi.advanceTimersByTime(250); // Half way through
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
rerender({ value: 'change2' });
|
|
50
|
+
expect(result.current).toBe('initial'); // Should still be initial
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
vi.advanceTimersByTime(250); // Half way through again
|
|
54
|
+
});
|
|
55
|
+
expect(result.current).toBe('initial'); // Should still be initial
|
|
56
|
+
|
|
57
|
+
act(() => {
|
|
58
|
+
vi.advanceTimersByTime(250); // Complete the full delay
|
|
59
|
+
});
|
|
60
|
+
expect(result.current).toBe('change2'); // Should now be the last value
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle different delay times', () => {
|
|
64
|
+
const { result, rerender } = renderHook(
|
|
65
|
+
({ value }) => useDebounce(value, 1000),
|
|
66
|
+
{ initialProps: { value: 'initial' } }
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
rerender({ value: 'changed' });
|
|
70
|
+
expect(result.current).toBe('initial');
|
|
71
|
+
|
|
72
|
+
act(() => {
|
|
73
|
+
vi.advanceTimersByTime(500);
|
|
74
|
+
});
|
|
75
|
+
expect(result.current).toBe('initial'); // Should still be initial
|
|
76
|
+
|
|
77
|
+
act(() => {
|
|
78
|
+
vi.advanceTimersByTime(500);
|
|
79
|
+
});
|
|
80
|
+
expect(result.current).toBe('changed'); // Should now be changed
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file UseFocusTrap Hook Unit Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Hooks/UseFocusTrap
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { render, screen } from '@testing-library/react';
|
|
11
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import { useFocusTrap } from '../useFocusTrap';
|
|
14
|
+
|
|
15
|
+
// Create a test component that uses the hook
|
|
16
|
+
const TestComponent = ({ options = {} }: { options?: any }) => {
|
|
17
|
+
const focusTrap = useFocusTrap(options);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div ref={focusTrap.containerRef as any} data-testid="container">
|
|
21
|
+
<button data-testid="button-1">Button 1</button>
|
|
22
|
+
<input data-testid="input-1" type="text" />
|
|
23
|
+
<button data-testid="button-2">Button 2</button>
|
|
24
|
+
<button data-testid="button-3" disabled>Disabled Button</button>
|
|
25
|
+
<div data-testid="non-focusable">Non-focusable</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('useFocusTrap', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Basic Functionality', () => {
|
|
40
|
+
it('should render without errors', () => {
|
|
41
|
+
expect(() => renderWithProviders(<TestComponent />)).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should render with container element', () => {
|
|
45
|
+
renderWithProviders(<TestComponent />);
|
|
46
|
+
expect(screen.getByTestId('container')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should render with focusable elements', () => {
|
|
50
|
+
renderWithProviders(<TestComponent />);
|
|
51
|
+
expect(screen.getByTestId('button-1')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByTestId('input-1')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByTestId('button-2')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle disabled elements', () => {
|
|
57
|
+
renderWithProviders(<TestComponent />);
|
|
58
|
+
const disabledButton = screen.getByTestId('button-3');
|
|
59
|
+
expect(disabledButton).toBeInTheDocument();
|
|
60
|
+
expect(disabledButton).toBeDisabled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle non-focusable elements', () => {
|
|
64
|
+
renderWithProviders(<TestComponent />);
|
|
65
|
+
expect(screen.getByTestId('non-focusable')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Hook Interface', () => {
|
|
70
|
+
it('should return focus trap interface', () => {
|
|
71
|
+
const TestHook = () => {
|
|
72
|
+
const focusTrap = useFocusTrap();
|
|
73
|
+
|
|
74
|
+
expect(focusTrap.containerRef).toBeDefined();
|
|
75
|
+
expect(typeof focusTrap.focusFirst).toBe('function');
|
|
76
|
+
expect(typeof focusTrap.focusLast).toBe('function');
|
|
77
|
+
expect(typeof focusTrap.getFocusableElements).toBe('function');
|
|
78
|
+
|
|
79
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
renderWithProviders(<TestHook />);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle default options', () => {
|
|
86
|
+
const TestHook = () => {
|
|
87
|
+
const focusTrap = useFocusTrap();
|
|
88
|
+
|
|
89
|
+
// Should work with default options
|
|
90
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
91
|
+
|
|
92
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
renderWithProviders(<TestHook />);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle custom options', () => {
|
|
99
|
+
const TestHook = () => {
|
|
100
|
+
const focusTrap = useFocusTrap({
|
|
101
|
+
isActive: true,
|
|
102
|
+
autoFocus: true,
|
|
103
|
+
restoreFocus: true,
|
|
104
|
+
onEscape: vi.fn(),
|
|
105
|
+
focusableSelector: 'button:not([disabled])'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
109
|
+
|
|
110
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
renderWithProviders(<TestHook />);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Focus Management Functions', () => {
|
|
118
|
+
it('should provide focus management functions', () => {
|
|
119
|
+
const TestHook = () => {
|
|
120
|
+
const focusTrap = useFocusTrap();
|
|
121
|
+
|
|
122
|
+
// Test that functions exist and can be called
|
|
123
|
+
expect(() => focusTrap.focusFirst()).not.toThrow();
|
|
124
|
+
expect(() => focusTrap.focusLast()).not.toThrow();
|
|
125
|
+
expect(() => focusTrap.getFocusableElements()).not.toThrow();
|
|
126
|
+
|
|
127
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
renderWithProviders(<TestHook />);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle empty focusable elements gracefully', () => {
|
|
134
|
+
const TestHook = () => {
|
|
135
|
+
const focusTrap = useFocusTrap();
|
|
136
|
+
|
|
137
|
+
// Should not throw when no focusable elements exist
|
|
138
|
+
expect(() => focusTrap.focusFirst()).not.toThrow();
|
|
139
|
+
expect(() => focusTrap.focusLast()).not.toThrow();
|
|
140
|
+
expect(() => focusTrap.getFocusableElements()).not.toThrow();
|
|
141
|
+
|
|
142
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
renderWithProviders(<TestHook />);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('Options Handling', () => {
|
|
150
|
+
it('should handle isActive option', () => {
|
|
151
|
+
const TestHook = () => {
|
|
152
|
+
const focusTrap = useFocusTrap({ isActive: true });
|
|
153
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
154
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
renderWithProviders(<TestHook />);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle autoFocus option', () => {
|
|
161
|
+
const TestHook = () => {
|
|
162
|
+
const focusTrap = useFocusTrap({ autoFocus: true });
|
|
163
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
164
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
renderWithProviders(<TestHook />);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle restoreFocus option', () => {
|
|
171
|
+
const TestHook = () => {
|
|
172
|
+
const focusTrap = useFocusTrap({ restoreFocus: true });
|
|
173
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
174
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
renderWithProviders(<TestHook />);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle onEscape option', () => {
|
|
181
|
+
const onEscape = vi.fn();
|
|
182
|
+
const TestHook = () => {
|
|
183
|
+
const focusTrap = useFocusTrap({ onEscape });
|
|
184
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
185
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
renderWithProviders(<TestHook />);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle custom focusableSelector', () => {
|
|
192
|
+
const TestHook = () => {
|
|
193
|
+
const focusTrap = useFocusTrap({
|
|
194
|
+
focusableSelector: 'button:not([disabled])'
|
|
195
|
+
});
|
|
196
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
197
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
renderWithProviders(<TestHook />);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Integration Tests', () => {
|
|
205
|
+
it('should work with multiple instances', () => {
|
|
206
|
+
const TestMultiple = () => {
|
|
207
|
+
const focusTrap1 = useFocusTrap({ isActive: true });
|
|
208
|
+
const focusTrap2 = useFocusTrap({ isActive: false });
|
|
209
|
+
|
|
210
|
+
expect(focusTrap1.containerRef.current).toBeDefined();
|
|
211
|
+
expect(focusTrap2.containerRef.current).toBeDefined();
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div>
|
|
215
|
+
<div ref={focusTrap1.containerRef as any} data-testid="container-1" />
|
|
216
|
+
<div ref={focusTrap2.containerRef as any} data-testid="container-2" />
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
renderWithProviders(<TestMultiple />);
|
|
222
|
+
expect(screen.getByTestId('container-1')).toBeInTheDocument();
|
|
223
|
+
expect(screen.getByTestId('container-2')).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle component unmounting', () => {
|
|
227
|
+
const { unmount } = renderWithProviders(<TestComponent />);
|
|
228
|
+
|
|
229
|
+
expect(() => unmount()).not.toThrow();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle all options together', () => {
|
|
233
|
+
const onEscape = vi.fn();
|
|
234
|
+
const TestAllOptions = () => {
|
|
235
|
+
const focusTrap = useFocusTrap({
|
|
236
|
+
isActive: true,
|
|
237
|
+
autoFocus: true,
|
|
238
|
+
restoreFocus: true,
|
|
239
|
+
onEscape,
|
|
240
|
+
focusableSelector: 'button:not([disabled])'
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
244
|
+
|
|
245
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
renderWithProviders(<TestAllOptions />);
|
|
249
|
+
expect(screen.getByTestId('container')).toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Edge Cases', () => {
|
|
254
|
+
it('should handle null container ref', () => {
|
|
255
|
+
const TestNullRef = () => {
|
|
256
|
+
const focusTrap = useFocusTrap();
|
|
257
|
+
|
|
258
|
+
// Test that functions don't throw even with null ref
|
|
259
|
+
expect(() => focusTrap.focusFirst()).not.toThrow();
|
|
260
|
+
expect(() => focusTrap.focusLast()).not.toThrow();
|
|
261
|
+
expect(() => focusTrap.getFocusableElements()).not.toThrow();
|
|
262
|
+
|
|
263
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
renderWithProviders(<TestNullRef />);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle undefined options', () => {
|
|
270
|
+
const TestUndefinedOptions = () => {
|
|
271
|
+
const focusTrap = useFocusTrap(undefined as any);
|
|
272
|
+
|
|
273
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
274
|
+
|
|
275
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
renderWithProviders(<TestUndefinedOptions />);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should handle partial options', () => {
|
|
282
|
+
const TestPartialOptions = () => {
|
|
283
|
+
const focusTrap = useFocusTrap({ isActive: true });
|
|
284
|
+
|
|
285
|
+
expect(focusTrap.containerRef.current).toBeDefined();
|
|
286
|
+
|
|
287
|
+
return <div ref={focusTrap.containerRef as any} data-testid="container" />;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
renderWithProviders(<TestPartialOptions />);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|