@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.
Files changed (170) hide show
  1. package/dist/{DataTable-ZQDRE46Q.js → DataTable-3SSI644S.js} +2 -2
  2. package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
  3. package/dist/chunk-2BJFM2JC.js.map +1 -0
  4. package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
  5. package/dist/components.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/styles/core.css +3 -0
  8. package/dist/utils.js +1 -1
  9. package/docs/api/classes/ErrorBoundary.md +1 -1
  10. package/docs/api/classes/InvalidScopeError.md +1 -1
  11. package/docs/api/classes/MissingUserContextError.md +1 -1
  12. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  13. package/docs/api/classes/PermissionDeniedError.md +1 -1
  14. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  15. package/docs/api/classes/RBACAuditManager.md +1 -1
  16. package/docs/api/classes/RBACCache.md +1 -1
  17. package/docs/api/classes/RBACEngine.md +1 -1
  18. package/docs/api/classes/RBACError.md +1 -1
  19. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  20. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  21. package/docs/api/interfaces/AggregateConfig.md +1 -1
  22. package/docs/api/interfaces/ButtonProps.md +1 -1
  23. package/docs/api/interfaces/CardProps.md +1 -1
  24. package/docs/api/interfaces/ColorPalette.md +1 -1
  25. package/docs/api/interfaces/ColorShade.md +1 -1
  26. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  27. package/docs/api/interfaces/DataTableAction.md +1 -1
  28. package/docs/api/interfaces/DataTableColumn.md +1 -1
  29. package/docs/api/interfaces/DataTableProps.md +34 -34
  30. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  31. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  32. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  33. package/docs/api/interfaces/EventContextType.md +1 -1
  34. package/docs/api/interfaces/EventLogoProps.md +1 -1
  35. package/docs/api/interfaces/EventProviderProps.md +1 -1
  36. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  37. package/docs/api/interfaces/FileUploadProps.md +1 -1
  38. package/docs/api/interfaces/FooterProps.md +1 -1
  39. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  40. package/docs/api/interfaces/InputProps.md +1 -1
  41. package/docs/api/interfaces/LabelProps.md +1 -1
  42. package/docs/api/interfaces/LoginFormProps.md +1 -1
  43. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  44. package/docs/api/interfaces/NavigationContextType.md +1 -1
  45. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  46. package/docs/api/interfaces/NavigationItem.md +1 -1
  47. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  48. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  49. package/docs/api/interfaces/Organisation.md +1 -1
  50. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  51. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  52. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  53. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  54. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  55. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  56. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  57. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  58. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  59. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  60. package/docs/api/interfaces/PaletteData.md +1 -1
  61. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  62. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  63. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  64. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  65. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  67. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  68. package/docs/api/interfaces/RBACConfig.md +1 -1
  69. package/docs/api/interfaces/RBACContextType.md +1 -1
  70. package/docs/api/interfaces/RBACLogger.md +1 -1
  71. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  72. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  73. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  74. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  75. package/docs/api/interfaces/RouteConfig.md +1 -1
  76. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  77. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  78. package/docs/api/interfaces/StorageConfig.md +1 -1
  79. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  80. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  81. package/docs/api/interfaces/StorageListOptions.md +1 -1
  82. package/docs/api/interfaces/StorageListResult.md +1 -1
  83. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  84. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  85. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  86. package/docs/api/interfaces/StyleImport.md +1 -1
  87. package/docs/api/interfaces/ToastActionElement.md +1 -1
  88. package/docs/api/interfaces/ToastProps.md +1 -1
  89. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  90. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  91. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  92. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  93. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  94. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  95. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  96. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  97. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  98. package/docs/api/interfaces/UserEventAccess.md +1 -1
  99. package/docs/api/interfaces/UserMenuProps.md +1 -1
  100. package/docs/api/interfaces/UserProfile.md +1 -1
  101. package/docs/api/modules.md +3 -3
  102. package/docs/implementation-guides/data-tables.md +20 -0
  103. package/docs/quick-reference.md +9 -0
  104. package/docs/rbac/examples.md +4 -0
  105. package/package.json +1 -1
  106. package/src/__tests__/helpers/test-utils.tsx +147 -1
  107. package/src/components/DataTable/DataTable.tsx +20 -0
  108. package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
  109. package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
  110. package/src/components/DataTable/components/DataTableCore.tsx +167 -138
  111. package/src/components/Header/Header.test.tsx +1 -1
  112. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
  113. package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
  114. package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
  115. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
  116. package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
  117. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
  118. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
  119. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
  120. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
  121. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
  122. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
  123. package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
  124. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
  125. package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
  126. package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
  127. package/src/rbac/api.test.ts +511 -0
  128. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  129. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  130. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  131. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  132. package/src/rbac/hooks/useCan.test.ts +1 -1
  133. package/src/rbac/hooks/usePermissions.test.ts +10 -5
  134. package/src/rbac/hooks/useRBAC.test.ts +141 -93
  135. package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
  136. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
  137. package/src/styles/core.css +3 -0
  138. package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
  139. package/src/utils/__tests__/audit.unit.test.ts +69 -0
  140. package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
  141. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
  142. package/src/utils/__tests__/cn.unit.test.ts +34 -0
  143. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
  144. package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
  145. package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
  146. package/src/utils/__tests__/formatting.unit.test.ts +66 -0
  147. package/src/utils/__tests__/index.unit.test.ts +251 -0
  148. package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
  149. package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
  150. package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
  151. package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
  152. package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
  153. package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
  154. package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
  155. package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
  156. package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
  157. package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
  158. package/src/utils/__tests__/security.unit.test.ts +127 -0
  159. package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
  160. package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
  161. package/src/utils/__tests__/validation.unit.test.ts +84 -0
  162. package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
  163. package/src/validation/__tests__/common.unit.test.ts +101 -0
  164. package/src/validation/__tests__/csrf.unit.test.ts +302 -0
  165. package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
  166. package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
  167. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
  168. package/dist/chunk-M4RW7PIP.js.map +0 -1
  169. /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-3SSI644S.js.map} +0 -0
  170. /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
+ });