@jmruthers/pace-core 0.5.61 → 0.5.63
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-5M6MV2VY.js → DataTable-7BER7PDS.js} +6 -6
- package/dist/{DataTable-DqDDvBfI.d.ts → DataTable-D15XipLZ.d.ts} +7 -0
- package/dist/{PublicLoadingSpinner-CrMOrhNz.d.ts → PublicLoadingSpinner-CXJ-W9wZ.d.ts} +2 -2
- package/dist/{chunk-44SAHU2N.js → chunk-2LPYEFXI.js} +5 -5
- package/dist/chunk-4BWGRQBG.js +74 -0
- package/dist/chunk-4BWGRQBG.js.map +1 -0
- package/dist/{chunk-XMTHMOOM.js → chunk-BTCA3ENN.js} +4 -4
- package/dist/{chunk-ESXTFEE6.js → chunk-C7GUF747.js} +3 -3
- package/dist/{chunk-W7PPXKTZ.js → chunk-CKNY7HYS.js} +2 -2
- package/dist/{chunk-5MLDIGHB.js → chunk-FVDOEGGG.js} +3 -3
- package/dist/{chunk-HFDYTSAP.js → chunk-QVEOQVD4.js} +3 -3
- package/dist/{chunk-XDXG6QVH.js → chunk-S66AJVI2.js} +13 -6
- package/dist/chunk-S66AJVI2.js.map +1 -0
- package/dist/{chunk-E4FPK232.js → chunk-T2MQY57J.js} +2 -2
- package/dist/{chunk-4ULBJNIT.js → chunk-T6HVDA24.js} +2 -2
- package/dist/{chunk-STT7INZR.js → chunk-ULBI5JGB.js} +2 -1
- package/dist/{chunk-CGSYCF2W.js → chunk-VTJ5HCZB.js} +2 -2
- package/dist/components.d.ts +82 -5
- package/dist/components.js +258 -9
- package/dist/components.js.map +1 -1
- package/dist/{appConfig-DjpeG6P-.d.ts → formatting-BfDeV-ja.d.ts} +29 -1
- package/dist/hooks.d.ts +3 -2
- package/dist/hooks.js +5 -5
- package/dist/index.d.ts +8 -11
- package/dist/index.js +25 -14
- package/dist/index.js.map +1 -1
- package/dist/{organisation-DD0yBbGU.d.ts → organisation-t-vvQC3g.d.ts} +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +4 -4
- package/dist/rbac/index.js +6 -6
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-Cu6oKazv.d.ts → usePublicRouteParams-CdoFxnJK.d.ts} +2 -63
- package/dist/useToast-Bm6TnSK-.d.ts +63 -0
- package/dist/utils.d.ts +3 -31
- package/dist/utils.js +8 -41
- package/dist/utils.js.map +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/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 +44 -18
- 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 +148 -26
- package/docs/implementation-guides/data-tables.md +67 -0
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.tsx +13 -0
- package/src/components/DataTable/__tests__/DataTable.default-state.test.tsx +414 -0
- package/src/components/DataTable/components/DataTableCore.tsx +19 -2
- package/src/components/DataTable/types.ts +9 -0
- package/src/components/Dialog/examples/__tests__/SmartDialogExample.unit.test.tsx +151 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +287 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +861 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +628 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +777 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +901 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/index.ts +15 -0
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +1 -1
- package/src/hooks/useFileReference.ts +37 -0
- package/src/index.ts +10 -0
- package/src/styles/base.css +208 -0
- package/src/styles/semantic.css +24 -0
- package/dist/chunk-W66AZIOH.js +0 -29
- package/dist/chunk-W66AZIOH.js.map +0 -1
- package/dist/chunk-XDXG6QVH.js.map +0 -1
- /package/dist/{DataTable-5M6MV2VY.js.map → DataTable-7BER7PDS.js.map} +0 -0
- /package/dist/{chunk-44SAHU2N.js.map → chunk-2LPYEFXI.js.map} +0 -0
- /package/dist/{chunk-XMTHMOOM.js.map → chunk-BTCA3ENN.js.map} +0 -0
- /package/dist/{chunk-ESXTFEE6.js.map → chunk-C7GUF747.js.map} +0 -0
- /package/dist/{chunk-W7PPXKTZ.js.map → chunk-CKNY7HYS.js.map} +0 -0
- /package/dist/{chunk-5MLDIGHB.js.map → chunk-FVDOEGGG.js.map} +0 -0
- /package/dist/{chunk-HFDYTSAP.js.map → chunk-QVEOQVD4.js.map} +0 -0
- /package/dist/{chunk-E4FPK232.js.map → chunk-T2MQY57J.js.map} +0 -0
- /package/dist/{chunk-4ULBJNIT.js.map → chunk-T6HVDA24.js.map} +0 -0
- /package/dist/{chunk-STT7INZR.js.map → chunk-ULBI5JGB.js.map} +0 -0
- /package/dist/{chunk-CGSYCF2W.js.map → chunk-VTJ5HCZB.js.map} +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
5
|
+
import '@testing-library/jest-dom';
|
|
6
|
+
// Define Operation type locally since old RBAC types are removed
|
|
7
|
+
type Operation = 'read' | 'create' | 'update' | 'delete' | 'manage';
|
|
8
|
+
|
|
9
|
+
// Mock React Router hooks
|
|
10
|
+
const mockNavigate = vi.fn();
|
|
11
|
+
const mockLocation = { pathname: '/test-path' };
|
|
12
|
+
vi.mock('react-router-dom', async () => {
|
|
13
|
+
const actual = await vi.importActual('react-router-dom');
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
useNavigate: () => mockNavigate,
|
|
17
|
+
useLocation: () => mockLocation,
|
|
18
|
+
Outlet: () => <div data-testid="mock-outlet">Mock Outlet Content</div>
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock UnifiedAuthProvider
|
|
23
|
+
const mockSignOut = vi.fn().mockResolvedValue({ error: null });
|
|
24
|
+
const mockUpdatePassword = vi.fn().mockResolvedValue({ error: null });
|
|
25
|
+
const mockUser = {
|
|
26
|
+
id: 'test-user-id',
|
|
27
|
+
email: 'test@example.com',
|
|
28
|
+
user_metadata: {
|
|
29
|
+
display_name: 'Test User',
|
|
30
|
+
organisationId: 'test-org-123',
|
|
31
|
+
eventId: 'test-event-456',
|
|
32
|
+
appId: 'test-app-789'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
vi.mock('../../../providers/UnifiedAuthProvider', () => ({
|
|
37
|
+
useUnifiedAuth: () => ({
|
|
38
|
+
user: mockUser,
|
|
39
|
+
signOut: mockSignOut,
|
|
40
|
+
updatePassword: mockUpdatePassword
|
|
41
|
+
})
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock OrganisationProvider
|
|
45
|
+
const mockOrganisation = {
|
|
46
|
+
id: 'test-org-id',
|
|
47
|
+
name: 'Test Organisation',
|
|
48
|
+
display_name: 'Test Organisation',
|
|
49
|
+
description: 'Test organisation for testing',
|
|
50
|
+
subscription_tier: 'basic',
|
|
51
|
+
settings: {},
|
|
52
|
+
is_active: true,
|
|
53
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
54
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockOrganisationContext = {
|
|
58
|
+
selectedOrganisation: mockOrganisation,
|
|
59
|
+
organisations: [mockOrganisation],
|
|
60
|
+
userMemberships: [{
|
|
61
|
+
id: 'test-membership-id',
|
|
62
|
+
user_id: 'test-user-id',
|
|
63
|
+
organisation_id: 'test-org-id',
|
|
64
|
+
role: 'org_admin',
|
|
65
|
+
granted_at: '2023-01-01T00:00:00Z',
|
|
66
|
+
status: 'active' as const,
|
|
67
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
68
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
69
|
+
}],
|
|
70
|
+
isLoading: false,
|
|
71
|
+
error: null,
|
|
72
|
+
hasValidOrganisationContext: true,
|
|
73
|
+
setSelectedOrganisation: vi.fn(),
|
|
74
|
+
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
75
|
+
getUserRole: vi.fn().mockReturnValue('member'),
|
|
76
|
+
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
77
|
+
ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation),
|
|
78
|
+
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
getPrimaryOrganisation: vi.fn().mockReturnValue(mockOrganisation),
|
|
80
|
+
isOrganisationSecure: vi.fn().mockReturnValue(true)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
vi.mock('../../../providers/OrganisationProvider', () => ({
|
|
84
|
+
OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
85
|
+
useOrganisations: () => mockOrganisationContext
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Mock the new RBAC system for performance testing
|
|
89
|
+
const mockIsPermitted = vi.fn().mockResolvedValue(true);
|
|
90
|
+
const mockCheckPermission = vi.fn().mockResolvedValue(true);
|
|
91
|
+
|
|
92
|
+
vi.mock('../../../rbac/api', () => ({
|
|
93
|
+
isPermitted: vi.fn().mockResolvedValue(true),
|
|
94
|
+
getPermissionMap: vi.fn().mockResolvedValue({}),
|
|
95
|
+
getAccessLevel: vi.fn().mockResolvedValue('viewer'),
|
|
96
|
+
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
97
|
+
setupRBAC: vi.fn()
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Mock child components
|
|
101
|
+
vi.mock('../../Header', () => ({
|
|
102
|
+
Header: vi.fn(({ appName, user, onSignOut, onChangePassword, onNavigate, currentPath, logo, userMenu, actions, navItems }) => (
|
|
103
|
+
<header data-testid="mock-header" role="banner">
|
|
104
|
+
<div data-testid="app-name">{appName ?? 'Test App'}</div>
|
|
105
|
+
<div data-testid="user-info">{user?.user_metadata?.display_name || user?.email}</div>
|
|
106
|
+
<nav data-testid="navigation">
|
|
107
|
+
{navItems?.map((item, index) => (
|
|
108
|
+
<button
|
|
109
|
+
key={item.id}
|
|
110
|
+
data-testid={`nav-nav-${index}`}
|
|
111
|
+
onClick={() => onNavigate(item)}
|
|
112
|
+
>
|
|
113
|
+
{item.label}
|
|
114
|
+
</button>
|
|
115
|
+
)) || (
|
|
116
|
+
<>
|
|
117
|
+
<button
|
|
118
|
+
data-testid="home-nav"
|
|
119
|
+
onClick={() => onNavigate({ id: 'home', label: 'Home', href: '/' })}
|
|
120
|
+
>
|
|
121
|
+
Home
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
data-testid="dashboard-nav"
|
|
125
|
+
onClick={() => onNavigate({ id: 'dashboard', label: 'Dashboard', href: '/dashboard' })}
|
|
126
|
+
>
|
|
127
|
+
Dashboard
|
|
128
|
+
</button>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</nav>
|
|
132
|
+
{logo && logo}
|
|
133
|
+
{userMenu && userMenu}
|
|
134
|
+
{actions && actions}
|
|
135
|
+
<button
|
|
136
|
+
data-testid="sign-out-button"
|
|
137
|
+
onClick={() => onSignOut()}
|
|
138
|
+
>
|
|
139
|
+
Sign Out
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
data-testid="change-password-button"
|
|
143
|
+
onClick={() => onChangePassword('newpassword123')}
|
|
144
|
+
>
|
|
145
|
+
Change Password
|
|
146
|
+
</button>
|
|
147
|
+
<div data-testid="current-path">{currentPath}</div>
|
|
148
|
+
</header>
|
|
149
|
+
))
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
vi.mock('../../Footer', () => ({
|
|
153
|
+
Footer: vi.fn(() => <footer data-testid="mock-footer" role="contentinfo">Mock Footer</footer>)
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
// Mock window.location
|
|
157
|
+
Object.defineProperty(window, 'location', {
|
|
158
|
+
value: {
|
|
159
|
+
pathname: '/test-path'
|
|
160
|
+
},
|
|
161
|
+
writable: true
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
import { PaceAppLayout } from '../PaceAppLayout';
|
|
165
|
+
|
|
166
|
+
// Wrapper component to provide Router context
|
|
167
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
168
|
+
<BrowserRouter>
|
|
169
|
+
{children}
|
|
170
|
+
</BrowserRouter>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Performance thresholds
|
|
174
|
+
const PERFORMANCE_THRESHOLDS = {
|
|
175
|
+
RENDER_TIME: 200, // ms - Increased due to migration changes requiring more complex organization loading
|
|
176
|
+
PERMISSION_CHECK_TIME: 110, // ms - Increased to account for timing variations
|
|
177
|
+
MEMORY_USAGE_INCREASE: 1024 * 1024, // 1MB
|
|
178
|
+
RE_RENDER_COUNT: 3
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
describe('PaceAppLayout Performance', () => {
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
// Reset all mocks before each test
|
|
184
|
+
vi.clearAllMocks();
|
|
185
|
+
mockSignOut.mockResolvedValue({ error: null });
|
|
186
|
+
mockUpdatePassword.mockResolvedValue({ error: null });
|
|
187
|
+
mockCheckPermission.mockClear();
|
|
188
|
+
mockCheckPermission.mockResolvedValue(true);
|
|
189
|
+
mockIsPermitted.mockClear();
|
|
190
|
+
mockIsPermitted.mockResolvedValue(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Rendering Performance', () => {
|
|
194
|
+
it('renders within performance threshold', () => {
|
|
195
|
+
const startTime = performance.now();
|
|
196
|
+
|
|
197
|
+
render(
|
|
198
|
+
<TestWrapper>
|
|
199
|
+
<PaceAppLayout appName="Test App" />
|
|
200
|
+
</TestWrapper>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const endTime = performance.now();
|
|
204
|
+
const renderTime = endTime - startTime;
|
|
205
|
+
|
|
206
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
207
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('renders with custom components within threshold', () => {
|
|
211
|
+
const CustomLogo = () => <div data-testid="custom-logo">Custom Logo</div>;
|
|
212
|
+
const CustomUserMenu = () => <div data-testid="custom-user-menu">Custom Menu</div>;
|
|
213
|
+
|
|
214
|
+
const startTime = performance.now();
|
|
215
|
+
|
|
216
|
+
render(
|
|
217
|
+
<TestWrapper>
|
|
218
|
+
<PaceAppLayout
|
|
219
|
+
appName="Test App"
|
|
220
|
+
customLogo={<CustomLogo />}
|
|
221
|
+
customUserMenu={<CustomUserMenu />}
|
|
222
|
+
/>
|
|
223
|
+
</TestWrapper>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const endTime = performance.now();
|
|
227
|
+
const renderTime = endTime - startTime;
|
|
228
|
+
|
|
229
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
230
|
+
expect(screen.getByTestId('custom-logo')).toBeInTheDocument();
|
|
231
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('renders with large navigation items within threshold', () => {
|
|
235
|
+
const largeNavItems = Array.from({ length: 50 }, (_, i) => ({
|
|
236
|
+
id: `nav-${i}`,
|
|
237
|
+
label: `Navigation ${i}`,
|
|
238
|
+
href: `/nav-${i}`
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
const startTime = performance.now();
|
|
242
|
+
|
|
243
|
+
render(
|
|
244
|
+
<TestWrapper>
|
|
245
|
+
<PaceAppLayout appName="Test App" navItems={largeNavItems} />
|
|
246
|
+
</TestWrapper>
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const endTime = performance.now();
|
|
250
|
+
const renderTime = endTime - startTime;
|
|
251
|
+
|
|
252
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
253
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('Permission Check Performance', () => {
|
|
258
|
+
it('performs permission checks within threshold', async () => {
|
|
259
|
+
const startTime = performance.now();
|
|
260
|
+
|
|
261
|
+
render(
|
|
262
|
+
<TestWrapper>
|
|
263
|
+
<PaceAppLayout appName="Test App" enforcePermissions={true} />
|
|
264
|
+
</TestWrapper>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 10)); // Wait for async operations
|
|
268
|
+
|
|
269
|
+
const endTime = performance.now();
|
|
270
|
+
const permissionCheckTime = endTime - startTime;
|
|
271
|
+
|
|
272
|
+
expect(permissionCheckTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
273
|
+
// Performance test - just verify the component renders within threshold
|
|
274
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('handles multiple permission checks efficiently', async () => {
|
|
278
|
+
const routePermissions: Record<string, Operation> = {
|
|
279
|
+
'/dashboard': 'read',
|
|
280
|
+
'/settings': 'update',
|
|
281
|
+
'/admin': 'delete'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const startTime = performance.now();
|
|
285
|
+
|
|
286
|
+
render(
|
|
287
|
+
<TestWrapper>
|
|
288
|
+
<PaceAppLayout
|
|
289
|
+
appName="Test App"
|
|
290
|
+
enforcePermissions={true}
|
|
291
|
+
routePermissions={routePermissions}
|
|
292
|
+
/>
|
|
293
|
+
</TestWrapper>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
297
|
+
|
|
298
|
+
const endTime = performance.now();
|
|
299
|
+
const permissionCheckTime = endTime - startTime;
|
|
300
|
+
|
|
301
|
+
expect(permissionCheckTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
302
|
+
// Performance test - just verify the component renders within threshold
|
|
303
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('handles permission check errors efficiently', async () => {
|
|
307
|
+
mockCheckPermission.mockRejectedValue(new Error('Permission check failed'));
|
|
308
|
+
|
|
309
|
+
const startTime = performance.now();
|
|
310
|
+
|
|
311
|
+
render(
|
|
312
|
+
<TestWrapper>
|
|
313
|
+
<PaceAppLayout appName="Test App" enforcePermissions={true} />
|
|
314
|
+
</TestWrapper>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
318
|
+
|
|
319
|
+
const endTime = performance.now();
|
|
320
|
+
const permissionCheckTime = endTime - startTime;
|
|
321
|
+
|
|
322
|
+
expect(permissionCheckTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Memory Usage', () => {
|
|
327
|
+
it('does not cause significant memory leaks on re-renders', () => {
|
|
328
|
+
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
329
|
+
|
|
330
|
+
const { rerender } = render(
|
|
331
|
+
<TestWrapper>
|
|
332
|
+
<PaceAppLayout appName="Test App" />
|
|
333
|
+
</TestWrapper>
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Perform multiple re-renders
|
|
337
|
+
for (let i = 0; i < PERFORMANCE_THRESHOLDS.RE_RENDER_COUNT; i++) {
|
|
338
|
+
rerender(
|
|
339
|
+
<TestWrapper>
|
|
340
|
+
<PaceAppLayout appName={`Test App ${i}`} />
|
|
341
|
+
</TestWrapper>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
346
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
347
|
+
|
|
348
|
+
if (initialMemory > 0 && finalMemory > 0) {
|
|
349
|
+
expect(memoryIncrease).toBeLessThan(PERFORMANCE_THRESHOLDS.MEMORY_USAGE_INCREASE);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('handles large navigation items without memory issues', () => {
|
|
354
|
+
const largeNavItems = Array.from({ length: 100 }, (_, i) => ({
|
|
355
|
+
id: `nav-${i}`,
|
|
356
|
+
label: `Navigation ${i}`,
|
|
357
|
+
href: `/nav-${i}`
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
361
|
+
|
|
362
|
+
render(
|
|
363
|
+
<TestWrapper>
|
|
364
|
+
<PaceAppLayout appName="Test App" navItems={largeNavItems} />
|
|
365
|
+
</TestWrapper>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
|
369
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
370
|
+
|
|
371
|
+
if (initialMemory > 0 && finalMemory > 0) {
|
|
372
|
+
expect(memoryIncrease).toBeLessThan(PERFORMANCE_THRESHOLDS.MEMORY_USAGE_INCREASE);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('Re-render Performance', () => {
|
|
378
|
+
it('handles prop changes efficiently', () => {
|
|
379
|
+
const { rerender } = render(
|
|
380
|
+
<TestWrapper>
|
|
381
|
+
<PaceAppLayout appName="Test App" />
|
|
382
|
+
</TestWrapper>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const startTime = performance.now();
|
|
386
|
+
|
|
387
|
+
// Change props multiple times
|
|
388
|
+
for (let i = 0; i < PERFORMANCE_THRESHOLDS.RE_RENDER_COUNT; i++) {
|
|
389
|
+
rerender(
|
|
390
|
+
<TestWrapper>
|
|
391
|
+
<PaceAppLayout
|
|
392
|
+
appName={`Test App ${i}`}
|
|
393
|
+
showEventSelector={i % 2 === 0}
|
|
394
|
+
showUserMenu={i % 2 === 1}
|
|
395
|
+
/>
|
|
396
|
+
</TestWrapper>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const endTime = performance.now();
|
|
401
|
+
const totalTime = endTime - startTime;
|
|
402
|
+
const averageTime = totalTime / PERFORMANCE_THRESHOLDS.RE_RENDER_COUNT;
|
|
403
|
+
|
|
404
|
+
expect(averageTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('handles permission enforcement toggles efficiently', async () => {
|
|
408
|
+
const { rerender } = render(
|
|
409
|
+
<TestWrapper>
|
|
410
|
+
<PaceAppLayout appName="Test App" />
|
|
411
|
+
</TestWrapper>
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const startTime = performance.now();
|
|
415
|
+
|
|
416
|
+
// Toggle permission enforcement
|
|
417
|
+
rerender(
|
|
418
|
+
<TestWrapper>
|
|
419
|
+
<PaceAppLayout appName="Test App" enforcePermissions={true} />
|
|
420
|
+
</TestWrapper>
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
424
|
+
|
|
425
|
+
rerender(
|
|
426
|
+
<TestWrapper>
|
|
427
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
428
|
+
</TestWrapper>
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const endTime = performance.now();
|
|
432
|
+
const totalTime = endTime - startTime;
|
|
433
|
+
|
|
434
|
+
expect(totalTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('Navigation Performance', () => {
|
|
439
|
+
it('handles rapid navigation efficiently', () => {
|
|
440
|
+
const navItems = [
|
|
441
|
+
{ id: 'home', label: 'Home', href: '/' },
|
|
442
|
+
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
|
|
443
|
+
{ id: 'settings', label: 'Settings', href: '/settings' },
|
|
444
|
+
{ id: 'ui-showcase', label: 'UI Showcase', href: '/ui-showcase' },
|
|
445
|
+
{ id: 'data-table-showcase', label: 'DataTable Showcase', href: '/data-table-showcase' }
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const startTime = performance.now();
|
|
449
|
+
|
|
450
|
+
render(
|
|
451
|
+
<TestWrapper>
|
|
452
|
+
<PaceAppLayout appName="Test App" navItems={navItems} enforcePermissions={false} />
|
|
453
|
+
</TestWrapper>
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const endTime = performance.now();
|
|
457
|
+
const renderTime = endTime - startTime;
|
|
458
|
+
|
|
459
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
460
|
+
|
|
461
|
+
// Rapidly click navigation buttons
|
|
462
|
+
// If not, this test will fail.
|
|
463
|
+
// @ts-ignore
|
|
464
|
+
fireEvent.click(screen.getByTestId('nav-nav-0'));
|
|
465
|
+
// @ts-ignore
|
|
466
|
+
fireEvent.click(screen.getByTestId('nav-nav-1'));
|
|
467
|
+
// @ts-ignore
|
|
468
|
+
fireEvent.click(screen.getByTestId('nav-nav-2'));
|
|
469
|
+
|
|
470
|
+
// Should handle rapid navigation without performance issues
|
|
471
|
+
expect(mockNavigate).toHaveBeenCalledTimes(3);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('handles large navigation items efficiently', () => {
|
|
475
|
+
const largeNavItems = Array.from({ length: 50 }, (_, i) => ({
|
|
476
|
+
id: `nav-${i}`,
|
|
477
|
+
label: `Navigation ${i}`,
|
|
478
|
+
href: `/nav-${i}`
|
|
479
|
+
}));
|
|
480
|
+
|
|
481
|
+
render(
|
|
482
|
+
<TestWrapper>
|
|
483
|
+
<PaceAppLayout appName="Test App" navItems={largeNavItems} />
|
|
484
|
+
</TestWrapper>
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const startTime = performance.now();
|
|
488
|
+
|
|
489
|
+
// Click on multiple navigation items
|
|
490
|
+
for (let i = 0; i < 5; i++) {
|
|
491
|
+
const navButton = screen.getByTestId(`nav-nav-${i}`);
|
|
492
|
+
// @ts-ignore
|
|
493
|
+
fireEvent.click(navButton);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const endTime = performance.now();
|
|
497
|
+
const navigationTime = endTime - startTime;
|
|
498
|
+
|
|
499
|
+
expect(navigationTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe('Authentication Performance', () => {
|
|
504
|
+
it('handles authentication actions efficiently', async () => {
|
|
505
|
+
// Reset mocks before test
|
|
506
|
+
mockSignOut.mockClear();
|
|
507
|
+
mockUpdatePassword.mockClear();
|
|
508
|
+
|
|
509
|
+
render(
|
|
510
|
+
<TestWrapper>
|
|
511
|
+
<PaceAppLayout appName="Test App" />
|
|
512
|
+
</TestWrapper>
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const startTime = performance.now();
|
|
516
|
+
|
|
517
|
+
// Perform authentication actions
|
|
518
|
+
const signOutButton = screen.getByTestId('sign-out-button');
|
|
519
|
+
const changePasswordButton = screen.getByTestId('change-password-button');
|
|
520
|
+
|
|
521
|
+
fireEvent.click(signOutButton);
|
|
522
|
+
fireEvent.click(changePasswordButton);
|
|
523
|
+
|
|
524
|
+
// Wait for async operations to complete
|
|
525
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
526
|
+
|
|
527
|
+
const endTime = performance.now();
|
|
528
|
+
const authTime = endTime - startTime;
|
|
529
|
+
|
|
530
|
+
expect(authTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
531
|
+
expect(mockSignOut).toHaveBeenCalled();
|
|
532
|
+
expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('handles authentication errors efficiently', async () => {
|
|
536
|
+
// Reset mocks and set up error responses
|
|
537
|
+
mockSignOut.mockClear();
|
|
538
|
+
mockUpdatePassword.mockClear();
|
|
539
|
+
mockSignOut.mockResolvedValue({ error: { message: 'Sign out failed' } });
|
|
540
|
+
mockUpdatePassword.mockResolvedValue({ error: { message: 'Password update failed' } });
|
|
541
|
+
|
|
542
|
+
render(
|
|
543
|
+
<TestWrapper>
|
|
544
|
+
<PaceAppLayout appName="Test App" />
|
|
545
|
+
</TestWrapper>
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const startTime = performance.now();
|
|
549
|
+
|
|
550
|
+
// Perform authentication actions that will fail
|
|
551
|
+
const signOutButton = screen.getByTestId('sign-out-button');
|
|
552
|
+
const changePasswordButton = screen.getByTestId('change-password-button');
|
|
553
|
+
|
|
554
|
+
fireEvent.click(signOutButton);
|
|
555
|
+
fireEvent.click(changePasswordButton);
|
|
556
|
+
|
|
557
|
+
// Wait for async operations to complete
|
|
558
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
559
|
+
|
|
560
|
+
const endTime = performance.now();
|
|
561
|
+
const authTime = endTime - startTime;
|
|
562
|
+
|
|
563
|
+
expect(authTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('Complex Configuration Performance', () => {
|
|
568
|
+
it('handles complex configurations efficiently', () => {
|
|
569
|
+
const customNavItems = Array.from({ length: 20 }, (_, i) => ({
|
|
570
|
+
id: `nav-${i}`,
|
|
571
|
+
label: `Navigation ${i}`,
|
|
572
|
+
href: `/nav-${i}`
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
const HeaderActions = () => (
|
|
576
|
+
<div data-testid="header-actions">
|
|
577
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
578
|
+
<button key={i} data-testid={`action-${i}`}>Action {i}</button>
|
|
579
|
+
))}
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const CustomUserMenu = () => (
|
|
584
|
+
<div data-testid="custom-user-menu">
|
|
585
|
+
{Array.from({ length: 3 }, (_, i) => (
|
|
586
|
+
<button key={i} data-testid={`menu-${i}`}>Menu {i}</button>
|
|
587
|
+
))}
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const startTime = performance.now();
|
|
592
|
+
|
|
593
|
+
render(
|
|
594
|
+
<TestWrapper>
|
|
595
|
+
<PaceAppLayout
|
|
596
|
+
appName="Complex App"
|
|
597
|
+
navItems={customNavItems}
|
|
598
|
+
headerActions={<HeaderActions />}
|
|
599
|
+
customUserMenu={<CustomUserMenu />}
|
|
600
|
+
showEventSelector={false}
|
|
601
|
+
showUserMenu={true}
|
|
602
|
+
headerClassName="complex-header-class"
|
|
603
|
+
enforcePermissions={false}
|
|
604
|
+
defaultPermission="update"
|
|
605
|
+
routePermissions={{
|
|
606
|
+
'/nav-0': 'read',
|
|
607
|
+
'/nav-1': 'update',
|
|
608
|
+
'/nav-2': 'delete'
|
|
609
|
+
}}
|
|
610
|
+
pageIdMapping={{
|
|
611
|
+
'/nav-0': 'page-0',
|
|
612
|
+
'/nav-1': 'page-1',
|
|
613
|
+
'/nav-2': 'page-2'
|
|
614
|
+
}}
|
|
615
|
+
filterNavigationByPermissions={true}
|
|
616
|
+
/>
|
|
617
|
+
</TestWrapper>
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const endTime = performance.now();
|
|
621
|
+
const renderTime = endTime - startTime;
|
|
622
|
+
|
|
623
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME);
|
|
624
|
+
expect(screen.getByTestId('header-actions')).toBeInTheDocument();
|
|
625
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
});
|