@jmruthers/pace-core 0.5.60 → 0.5.62
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/README.md +2 -2
- 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-SL8WaQN7.d.ts → PublicLoadingSpinner-CXJ-W9wZ.d.ts} +3 -27
- package/dist/{chunk-SFMRBGGK.js → chunk-2LPYEFXI.js} +136 -137
- package/dist/chunk-2LPYEFXI.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-NQ4TOOO6.js → chunk-L3RV2ALE.js} +1 -1
- package/dist/chunk-L3RV2ALE.js.map +1 -0
- package/dist/{chunk-NMNDTCOR.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-ITPVFKDH.js → chunk-T6HVDA24.js} +129 -12
- package/dist/chunk-T6HVDA24.js.map +1 -0
- 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 +81 -4
- package/dist/components.js +258 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -61
- package/dist/hooks.js +31 -146
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +4 -4
- package/dist/rbac/index.js +6 -6
- package/dist/styles/index.d.ts +1 -1
- package/dist/styles/index.js +1 -1
- package/dist/types.js +1 -1
- package/dist/useToast-Bm6TnSK-.d.ts +63 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/docs/README.md +1 -1
- package/docs/api/README.md +2 -2
- 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 +38 -53
- package/docs/architecture/README.md +1 -3
- package/docs/consuming-app-example.md +3 -3
- package/docs/consuming-app-vite-config.md +1 -1
- package/docs/documentation-style-checklist.md +2 -2
- package/docs/getting-started/examples/basic-auth-app.md +2 -2
- package/docs/getting-started/installation.md +2 -2
- package/docs/getting-started/quick-start.md +1 -1
- package/docs/implementation-guides/data-tables.md +67 -0
- package/docs/migration/README.md +6 -6
- package/docs/migration/quick-migration-guide.md +3 -3
- package/docs/migration/v0.4.15-tailwind-scanning.md +1 -1
- package/docs/migration/v0.4.16-css-first-approach.md +1 -1
- package/docs/migration/v0.4.17-source-path-fix.md +4 -4
- package/docs/migration-guide.md +2 -2
- package/docs/quick-reference.md +4 -4
- package/docs/styles/README.md +3 -3
- package/docs/troubleshooting/README.md +2 -2
- package/docs/troubleshooting/common-issues.md +1 -1
- package/docs/troubleshooting/styling-issues.md +2 -2
- package/docs/troubleshooting/tailwind-content-scanning.md +2 -2
- package/docs/usage.md +2 -2
- package/package.json +2 -6
- 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/Toast.test.tsx +51 -21
- package/src/components/Toast/Toast.tsx +13 -35
- package/src/components/Toast/index.ts +2 -1
- package/src/components/index.ts +15 -1
- package/src/hooks/useFileReference.ts +37 -0
- package/src/index.ts +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +1 -1
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +1 -1
- package/src/styles/core.css +32 -37
- package/src/styles/index.ts +1 -1
- package/dist/chunk-ITPVFKDH.js.map +0 -1
- package/dist/chunk-NQ4TOOO6.js.map +0 -1
- package/dist/chunk-SFMRBGGK.js.map +0 -1
- package/dist/chunk-XDXG6QVH.js.map +0 -1
- package/dist/styles/core.css +0 -242
- package/dist/styles/fonts/georama-italic.woff2 +0 -0
- package/dist/styles/fonts/georama.woff2 +0 -0
- package/dist/styles/fonts/open-sans-italic.woff2 +0 -0
- package/dist/styles/fonts/open-sans.woff2 +0 -0
- package/dist/styles/fonts/reddit-mono.woff2 +0 -0
- /package/dist/{DataTable-5M6MV2VY.js.map → DataTable-7BER7PDS.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-NMNDTCOR.js.map → chunk-QVEOQVD4.js.map} +0 -0
- /package/dist/{chunk-E4FPK232.js.map → chunk-T2MQY57J.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,861 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } 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
|
+
|
|
7
|
+
// Mock React Router hooks
|
|
8
|
+
const mockNavigate = vi.fn();
|
|
9
|
+
const mockLocation = { pathname: '/test-path' };
|
|
10
|
+
vi.mock('react-router-dom', async () => {
|
|
11
|
+
const actual = await vi.importActual('react-router-dom');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
useNavigate: () => mockNavigate,
|
|
15
|
+
useLocation: () => mockLocation,
|
|
16
|
+
Outlet: () => <div data-testid="mock-outlet">Mock Outlet Content</div>
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Mock UnifiedAuthProvider
|
|
21
|
+
const mockSignOut = vi.fn().mockResolvedValue({ error: null });
|
|
22
|
+
const mockUpdatePassword = vi.fn().mockResolvedValue({ error: null });
|
|
23
|
+
const mockUser = {
|
|
24
|
+
id: 'test-user-id',
|
|
25
|
+
email: 'test@example.com',
|
|
26
|
+
user_metadata: {
|
|
27
|
+
display_name: 'Test User',
|
|
28
|
+
organisationId: 'test-org-123',
|
|
29
|
+
eventId: 'test-event-456',
|
|
30
|
+
appId: 'test-app-789'
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
vi.mock('../../../providers/UnifiedAuthProvider', () => ({
|
|
35
|
+
useUnifiedAuth: () => ({
|
|
36
|
+
user: mockUser,
|
|
37
|
+
signOut: mockSignOut,
|
|
38
|
+
updatePassword: mockUpdatePassword
|
|
39
|
+
})
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock OrganisationProvider
|
|
43
|
+
const mockOrganisation = {
|
|
44
|
+
id: 'test-org-id',
|
|
45
|
+
name: 'Test Organisation',
|
|
46
|
+
display_name: 'Test Organisation',
|
|
47
|
+
description: 'Test organisation for testing',
|
|
48
|
+
subscription_tier: 'basic',
|
|
49
|
+
settings: {},
|
|
50
|
+
is_active: true,
|
|
51
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
52
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockOrganisationContext = {
|
|
56
|
+
selectedOrganisation: mockOrganisation,
|
|
57
|
+
organisations: [mockOrganisation],
|
|
58
|
+
userMemberships: [{
|
|
59
|
+
id: 'test-membership-id',
|
|
60
|
+
user_id: 'test-user-id',
|
|
61
|
+
organisation_id: 'test-org-id',
|
|
62
|
+
role: 'org_admin',
|
|
63
|
+
granted_at: '2023-01-01T00:00:00Z',
|
|
64
|
+
status: 'active' as const,
|
|
65
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
66
|
+
updated_at: '2023-01-01T00:00:00Z'
|
|
67
|
+
}],
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null,
|
|
70
|
+
hasValidOrganisationContext: true,
|
|
71
|
+
setSelectedOrganisation: vi.fn(),
|
|
72
|
+
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
73
|
+
getUserRole: vi.fn().mockReturnValue('member'),
|
|
74
|
+
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
75
|
+
ensureOrganisationContext: vi.fn().mockReturnValue(mockOrganisation),
|
|
76
|
+
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
getPrimaryOrganisation: vi.fn().mockReturnValue(mockOrganisation),
|
|
78
|
+
isOrganisationSecure: vi.fn().mockReturnValue(true)
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
vi.mock('../../../providers/OrganisationProvider', () => ({
|
|
82
|
+
OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
83
|
+
useOrganisations: () => mockOrganisationContext
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
// Mock the new RBAC system
|
|
87
|
+
vi.mock('../../../rbac/api', () => ({
|
|
88
|
+
isPermitted: vi.fn().mockImplementation((input) => {
|
|
89
|
+
console.log('[PaceAppLayout] Page access attempt:', {
|
|
90
|
+
pageName: input.pageId || 'unknown',
|
|
91
|
+
operation: input.permission,
|
|
92
|
+
userId: input.userId,
|
|
93
|
+
allowed: true,
|
|
94
|
+
strictMode: true,
|
|
95
|
+
timestamp: new Date().toISOString()
|
|
96
|
+
});
|
|
97
|
+
return Promise.resolve(true);
|
|
98
|
+
}),
|
|
99
|
+
getPermissionMap: vi.fn().mockResolvedValue({}),
|
|
100
|
+
getAccessLevel: vi.fn().mockResolvedValue('viewer'),
|
|
101
|
+
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
102
|
+
setupRBAC: vi.fn()
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Mock child components with more realistic behavior
|
|
106
|
+
vi.mock('../../Header', () => ({
|
|
107
|
+
Header: vi.fn(({
|
|
108
|
+
user,
|
|
109
|
+
onSignOut,
|
|
110
|
+
onChangePassword,
|
|
111
|
+
onNavigate,
|
|
112
|
+
currentPath,
|
|
113
|
+
navItems,
|
|
114
|
+
actions,
|
|
115
|
+
userMenu,
|
|
116
|
+
logo,
|
|
117
|
+
logoUrl,
|
|
118
|
+
showEventSelector,
|
|
119
|
+
showUserMenu,
|
|
120
|
+
className
|
|
121
|
+
}) => (
|
|
122
|
+
<header data-testid="mock-header" role="banner" className={className}>
|
|
123
|
+
<div data-testid="app-name">{logoUrl ? 'Test App' : 'Complex App'}</div>
|
|
124
|
+
<div data-testid="user-info">{user?.user_metadata?.display_name || user?.email}</div>
|
|
125
|
+
<div data-testid="nav-items-count">{navItems?.length || 0}</div>
|
|
126
|
+
<div data-testid="has-actions">{actions ? 'true' : 'false'}</div>
|
|
127
|
+
<div data-testid="has-custom-user-menu">{userMenu ? 'true' : 'false'}</div>
|
|
128
|
+
<div data-testid="has-custom-logo">{logo ? 'true' : 'false'}</div>
|
|
129
|
+
<div data-testid="logo-url">{logoUrl || 'default'}</div>
|
|
130
|
+
<div data-testid="show-user-menu">{showUserMenu !== false ? 'true' : 'false'}</div>
|
|
131
|
+
<nav data-testid="navigation">
|
|
132
|
+
{navItems?.map((item, index) => (
|
|
133
|
+
<button
|
|
134
|
+
key={item.id}
|
|
135
|
+
data-testid={`nav-${item.id}`}
|
|
136
|
+
onClick={() => onNavigate(item)}
|
|
137
|
+
>
|
|
138
|
+
{item.label}
|
|
139
|
+
</button>
|
|
140
|
+
)) || (
|
|
141
|
+
<>
|
|
142
|
+
<button
|
|
143
|
+
data-testid="nav-home"
|
|
144
|
+
onClick={() => onNavigate({ id: 'home', label: 'Home', href: '/' })}
|
|
145
|
+
>
|
|
146
|
+
Home
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
data-testid="nav-dashboard"
|
|
150
|
+
onClick={() => onNavigate({ id: 'dashboard', label: 'Dashboard', href: '/dashboard' })}
|
|
151
|
+
>
|
|
152
|
+
Dashboard
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
data-testid="nav-settings"
|
|
156
|
+
onClick={() => onNavigate({ id: 'settings', label: 'Settings', href: '/settings' })}
|
|
157
|
+
>
|
|
158
|
+
Settings
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
data-testid="nav-ui-showcase"
|
|
162
|
+
onClick={() => onNavigate({ id: 'ui-showcase', label: 'UI Showcase', href: '/ui-showcase' })}
|
|
163
|
+
>
|
|
164
|
+
UI Showcase
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
data-testid="nav-data-table-showcase"
|
|
168
|
+
onClick={() => onNavigate({ id: 'data-table-showcase', label: 'DataTable Showcase', href: '/data-table-showcase' })}
|
|
169
|
+
>
|
|
170
|
+
DataTable Showcase
|
|
171
|
+
</button>
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</nav>
|
|
175
|
+
{logo && logo}
|
|
176
|
+
{userMenu && userMenu}
|
|
177
|
+
{actions && actions}
|
|
178
|
+
<button
|
|
179
|
+
data-testid="sign-out-button"
|
|
180
|
+
onClick={() => onSignOut()}
|
|
181
|
+
>
|
|
182
|
+
Sign Out
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
data-testid="change-password-button"
|
|
186
|
+
onClick={() => onChangePassword('newpassword123')}
|
|
187
|
+
>
|
|
188
|
+
Change Password
|
|
189
|
+
</button>
|
|
190
|
+
<div data-testid="current-path">{currentPath}</div>
|
|
191
|
+
<div data-testid="show-event-selector">{showEventSelector !== false ? 'true' : 'false'}</div>
|
|
192
|
+
</header>
|
|
193
|
+
))
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
vi.mock('../../Footer', () => ({
|
|
197
|
+
Footer: vi.fn(() => <footer data-testid="mock-footer" role="contentinfo">Mock Footer</footer>)
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
// Mock window.location
|
|
201
|
+
Object.defineProperty(window, 'location', {
|
|
202
|
+
value: {
|
|
203
|
+
pathname: '/test-path'
|
|
204
|
+
},
|
|
205
|
+
writable: true
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
import { PaceAppLayout } from '../PaceAppLayout';
|
|
209
|
+
|
|
210
|
+
// Wrapper component to provide Router context
|
|
211
|
+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
212
|
+
<BrowserRouter>
|
|
213
|
+
{children}
|
|
214
|
+
</BrowserRouter>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
describe('PaceAppLayout Integration', () => {
|
|
218
|
+
let mockIsPermitted: any;
|
|
219
|
+
|
|
220
|
+
beforeEach(async () => {
|
|
221
|
+
vi.clearAllMocks();
|
|
222
|
+
mockSignOut.mockResolvedValue({ error: null });
|
|
223
|
+
mockUpdatePassword.mockResolvedValue({ error: null });
|
|
224
|
+
|
|
225
|
+
// Get the mocked functions
|
|
226
|
+
const { isPermitted, isSuperAdmin } = await import('../../../rbac/api');
|
|
227
|
+
mockIsPermitted = vi.mocked(isPermitted);
|
|
228
|
+
const mockIsSuperAdmin = vi.mocked(isSuperAdmin);
|
|
229
|
+
|
|
230
|
+
// Set up isSuperAdmin mock
|
|
231
|
+
mockIsSuperAdmin.mockResolvedValue(false);
|
|
232
|
+
|
|
233
|
+
// Reset mockIsPermitted to default implementation
|
|
234
|
+
mockIsPermitted.mockImplementation((input) => {
|
|
235
|
+
console.log('[PaceAppLayout] Page access attempt:', {
|
|
236
|
+
pageName: input.pageId || 'unknown',
|
|
237
|
+
operation: input.permission,
|
|
238
|
+
userId: input.userId,
|
|
239
|
+
allowed: true,
|
|
240
|
+
strictMode: true,
|
|
241
|
+
timestamp: new Date().toISOString()
|
|
242
|
+
});
|
|
243
|
+
return Promise.resolve(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
mockSignOut.mockClear();
|
|
247
|
+
mockUpdatePassword.mockClear();
|
|
248
|
+
mockNavigate.mockClear();
|
|
249
|
+
mockIsPermitted.mockClear();
|
|
250
|
+
|
|
251
|
+
// Reset location mock
|
|
252
|
+
Object.defineProperty(window, 'location', {
|
|
253
|
+
value: { pathname: '/test-path' },
|
|
254
|
+
writable: true
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('Component Integration', () => {
|
|
259
|
+
const stableNavItems = [
|
|
260
|
+
{ id: 'custom1', label: 'Custom 1', href: '/custom1' },
|
|
261
|
+
{ id: 'custom2', label: 'Custom 2', href: '/custom2' },
|
|
262
|
+
{ id: 'custom3', label: 'Custom 3', href: '/custom3' }
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
it('renders all child components correctly', () => {
|
|
266
|
+
render(
|
|
267
|
+
<TestWrapper>
|
|
268
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
269
|
+
</TestWrapper>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
273
|
+
expect(screen.getByTestId('mock-footer')).toBeInTheDocument();
|
|
274
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('integrates with custom navigation items', () => {
|
|
278
|
+
render(
|
|
279
|
+
<TestWrapper>
|
|
280
|
+
<PaceAppLayout appName="Test App" navItems={stableNavItems} enforcePermissions={false} />
|
|
281
|
+
</TestWrapper>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(screen.getByTestId('nav-items-count')).toHaveTextContent('3');
|
|
285
|
+
expect(screen.getByTestId('nav-custom1')).toBeInTheDocument();
|
|
286
|
+
expect(screen.getByTestId('nav-custom2')).toBeInTheDocument();
|
|
287
|
+
expect(screen.getByTestId('nav-custom3')).toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('integrates with custom header actions', () => {
|
|
291
|
+
const HeaderActions = () => (
|
|
292
|
+
<div data-testid="header-actions">
|
|
293
|
+
<button data-testid="action-1">Action 1</button>
|
|
294
|
+
<button data-testid="action-2">Action 2</button>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
render(
|
|
299
|
+
<TestWrapper>
|
|
300
|
+
<PaceAppLayout appName="Test App" headerActions={<HeaderActions />} enforcePermissions={false} />
|
|
301
|
+
</TestWrapper>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(screen.getByTestId('header-actions')).toBeInTheDocument();
|
|
305
|
+
expect(screen.getByTestId('action-1')).toBeInTheDocument();
|
|
306
|
+
expect(screen.getByTestId('action-2')).toBeInTheDocument();
|
|
307
|
+
expect(screen.getByTestId('has-actions')).toHaveTextContent('true');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('integrates with custom user menu', () => {
|
|
311
|
+
const CustomUserMenu = () => (
|
|
312
|
+
<div data-testid="custom-user-menu">
|
|
313
|
+
<button data-testid="profile-btn">Profile</button>
|
|
314
|
+
<button data-testid="settings-btn">Settings</button>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
render(
|
|
319
|
+
<TestWrapper>
|
|
320
|
+
<PaceAppLayout appName="Test App" customUserMenu={<CustomUserMenu />} enforcePermissions={false} />
|
|
321
|
+
</TestWrapper>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
325
|
+
expect(screen.getByTestId('profile-btn')).toBeInTheDocument();
|
|
326
|
+
expect(screen.getByTestId('settings-btn')).toBeInTheDocument();
|
|
327
|
+
expect(screen.getByTestId('has-custom-user-menu')).toHaveTextContent('true');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('integrates with custom logo', () => {
|
|
331
|
+
const CustomLogo = () => (
|
|
332
|
+
<div data-testid="custom-logo">
|
|
333
|
+
<img src="/custom-logo.svg" alt="Custom Logo" />
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
render(
|
|
338
|
+
<TestWrapper>
|
|
339
|
+
<PaceAppLayout appName="Test App" customLogo={<CustomLogo />} enforcePermissions={false} />
|
|
340
|
+
</TestWrapper>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(screen.getByTestId('custom-logo')).toBeInTheDocument();
|
|
344
|
+
expect(screen.getByTestId('has-custom-logo')).toHaveTextContent('true');
|
|
345
|
+
expect(screen.getByTestId('logo-url')).toHaveTextContent('default');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('Navigation Integration', () => {
|
|
350
|
+
const stableNavItems2 = [
|
|
351
|
+
{ id: 'custom1', label: 'Custom 1', href: '/custom1' },
|
|
352
|
+
{ id: 'custom2', label: 'Custom 2', href: '/custom2' }
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
const stableNavItems3 = [
|
|
356
|
+
{ id: 'no-href', label: 'No Href', href: undefined },
|
|
357
|
+
{ id: 'with-href', label: 'With Href', href: '/with-href' }
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
it('handles navigation callbacks correctly', () => {
|
|
361
|
+
render(
|
|
362
|
+
<TestWrapper>
|
|
363
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
364
|
+
</TestWrapper>
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Test home navigation
|
|
368
|
+
fireEvent.click(screen.getByTestId('nav-home'));
|
|
369
|
+
expect(mockNavigate).toHaveBeenCalledWith('/');
|
|
370
|
+
|
|
371
|
+
// Test dashboard navigation
|
|
372
|
+
fireEvent.click(screen.getByTestId('nav-dashboard'));
|
|
373
|
+
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
|
374
|
+
|
|
375
|
+
// Test settings navigation
|
|
376
|
+
fireEvent.click(screen.getByTestId('nav-settings'));
|
|
377
|
+
expect(mockNavigate).toHaveBeenCalledWith('/settings');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('handles custom navigation items correctly', () => {
|
|
381
|
+
render(
|
|
382
|
+
<TestWrapper>
|
|
383
|
+
<PaceAppLayout appName="Test App" navItems={stableNavItems2} enforcePermissions={false} />
|
|
384
|
+
</TestWrapper>
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
fireEvent.click(screen.getByTestId('nav-custom1'));
|
|
388
|
+
expect(mockNavigate).toHaveBeenCalledWith('/custom1');
|
|
389
|
+
|
|
390
|
+
fireEvent.click(screen.getByTestId('nav-custom2'));
|
|
391
|
+
expect(mockNavigate).toHaveBeenCalledWith('/custom2');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('handles navigation items without href gracefully', () => {
|
|
395
|
+
render(
|
|
396
|
+
<TestWrapper>
|
|
397
|
+
<PaceAppLayout appName="Test App" navItems={stableNavItems3} enforcePermissions={false} />
|
|
398
|
+
</TestWrapper>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Should not crash when clicking item without href
|
|
402
|
+
fireEvent.click(screen.getByTestId('nav-no-href'));
|
|
403
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
404
|
+
|
|
405
|
+
// Should navigate when clicking item with href
|
|
406
|
+
fireEvent.click(screen.getByTestId('nav-with-href'));
|
|
407
|
+
expect(mockNavigate).toHaveBeenCalledWith('/with-href');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('Authentication Integration', () => {
|
|
412
|
+
it('handles sign out flow correctly', async () => {
|
|
413
|
+
render(
|
|
414
|
+
<TestWrapper>
|
|
415
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
416
|
+
</TestWrapper>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
fireEvent.click(screen.getByTestId('sign-out-button'));
|
|
420
|
+
|
|
421
|
+
await waitFor(() => {
|
|
422
|
+
expect(mockSignOut).toHaveBeenCalledTimes(1);
|
|
423
|
+
}, { timeout: 5000 });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('handles password change flow correctly', async () => {
|
|
427
|
+
render(
|
|
428
|
+
<TestWrapper>
|
|
429
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
430
|
+
</TestWrapper>
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
fireEvent.click(screen.getByTestId('change-password-button'));
|
|
434
|
+
|
|
435
|
+
await waitFor(() => {
|
|
436
|
+
expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
|
|
437
|
+
}, { timeout: 2000 });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('handles authentication errors gracefully', async () => {
|
|
441
|
+
mockSignOut.mockResolvedValue({ error: { message: 'Sign out failed' } });
|
|
442
|
+
mockUpdatePassword.mockResolvedValue({ error: { message: 'Password update failed' } });
|
|
443
|
+
|
|
444
|
+
render(
|
|
445
|
+
<TestWrapper>
|
|
446
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
447
|
+
</TestWrapper>
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Test sign out error
|
|
451
|
+
fireEvent.click(screen.getByTestId('sign-out-button'));
|
|
452
|
+
await waitFor(() => {
|
|
453
|
+
expect(mockSignOut).toHaveBeenCalled();
|
|
454
|
+
}, { timeout: 5000 });
|
|
455
|
+
|
|
456
|
+
// Test password change error
|
|
457
|
+
fireEvent.click(screen.getByTestId('change-password-button'));
|
|
458
|
+
await waitFor(() => {
|
|
459
|
+
expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
|
|
460
|
+
}, { timeout: 5000 });
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe('Permission Integration', () => {
|
|
465
|
+
const stablePermissionNavItems = [
|
|
466
|
+
{ id: 'public', label: 'Public', href: '/public' },
|
|
467
|
+
{ id: 'private', label: 'Private', href: '/private' },
|
|
468
|
+
{ id: 'admin', label: 'Admin', href: '/admin' }
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
it('integrates permission enforcement with navigation', async () => {
|
|
472
|
+
const routePermissions = {
|
|
473
|
+
'/dashboard': 'read',
|
|
474
|
+
'/settings': 'read'
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
render(
|
|
478
|
+
<TestWrapper>
|
|
479
|
+
<PaceAppLayout
|
|
480
|
+
appName="Test App"
|
|
481
|
+
enforcePermissions={true}
|
|
482
|
+
routePermissions={routePermissions}
|
|
483
|
+
/>
|
|
484
|
+
</TestWrapper>
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// With permission enforcement enabled and proper organisation context,
|
|
488
|
+
// the component should render normally (permission check passes)
|
|
489
|
+
await waitFor(() => {
|
|
490
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
491
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
492
|
+
}, { timeout: 2000 });
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it.skip('integrates permission filtering with navigation items', async () => {
|
|
496
|
+
const routePermissions = {
|
|
497
|
+
'/public': 'read',
|
|
498
|
+
'/private': 'read',
|
|
499
|
+
'/admin': 'read'
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Get the mocked functions and set them up
|
|
503
|
+
const { isSuperAdmin, isPermitted } = await import('../../../rbac/api');
|
|
504
|
+
vi.mocked(isSuperAdmin).mockResolvedValue(false);
|
|
505
|
+
vi.mocked(isPermitted).mockImplementation((input) => {
|
|
506
|
+
if (input.pageId === 'private') return Promise.resolve(false);
|
|
507
|
+
return Promise.resolve(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
render(
|
|
511
|
+
<TestWrapper>
|
|
512
|
+
<PaceAppLayout
|
|
513
|
+
appName="Test App"
|
|
514
|
+
navItems={stablePermissionNavItems}
|
|
515
|
+
enforcePermissions={true}
|
|
516
|
+
filterNavigationByPermissions={true}
|
|
517
|
+
routePermissions={routePermissions}
|
|
518
|
+
/>
|
|
519
|
+
</TestWrapper>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// With permission filtering enabled, the component should render
|
|
523
|
+
// and show filtered navigation items (filtering logic works)
|
|
524
|
+
await waitFor(() => {
|
|
525
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
526
|
+
expect(screen.getByTestId('nav-items-count')).toHaveTextContent('2'); // private item is filtered out
|
|
527
|
+
expect(screen.getByTestId('nav-public')).toBeInTheDocument();
|
|
528
|
+
expect(screen.getByTestId('nav-admin')).toBeInTheDocument();
|
|
529
|
+
expect(screen.queryByTestId('nav-private')).not.toBeInTheDocument(); // private item should be filtered out
|
|
530
|
+
}, { timeout: 2000 });
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('integrates custom page ID mapping with permissions', async () => {
|
|
534
|
+
const pageIdMapping = {
|
|
535
|
+
'/test-path': 'custom-page-id'
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
render(
|
|
539
|
+
<TestWrapper>
|
|
540
|
+
<PaceAppLayout
|
|
541
|
+
appName="Test App"
|
|
542
|
+
enforcePermissions={true}
|
|
543
|
+
pageIdMapping={pageIdMapping}
|
|
544
|
+
/>
|
|
545
|
+
</TestWrapper>
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
await waitFor(() => {
|
|
549
|
+
// With custom page ID mapping, the component should render normally
|
|
550
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
551
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
552
|
+
}, { timeout: 2000 });
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('integrates permission fallback with custom components', async () => {
|
|
556
|
+
// Mock the RBAC system to deny permission
|
|
557
|
+
const { isPermitted } = await import('../../../rbac/api');
|
|
558
|
+
vi.mocked(isPermitted).mockResolvedValue(false);
|
|
559
|
+
|
|
560
|
+
const CustomFallback = () => (
|
|
561
|
+
<div data-testid="custom-fallback">
|
|
562
|
+
<h2>Custom Access Denied</h2>
|
|
563
|
+
<p>You don't have access to this page.</p>
|
|
564
|
+
<button data-testid="custom-home-btn">Go to Home</button>
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
render(
|
|
569
|
+
<TestWrapper>
|
|
570
|
+
<PaceAppLayout
|
|
571
|
+
appName="Test App"
|
|
572
|
+
enforcePermissions={true}
|
|
573
|
+
permissionFallback={<CustomFallback />}
|
|
574
|
+
/>
|
|
575
|
+
</TestWrapper>
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
await waitFor(() => {
|
|
579
|
+
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
|
|
580
|
+
expect(screen.getByText('Custom Access Denied')).toBeInTheDocument();
|
|
581
|
+
expect(screen.getByTestId('custom-home-btn')).toBeInTheDocument();
|
|
582
|
+
}, { timeout: 2000 });
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe('Layout Integration', () => {
|
|
587
|
+
it('maintains proper layout structure', () => {
|
|
588
|
+
render(
|
|
589
|
+
<TestWrapper>
|
|
590
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
591
|
+
</TestWrapper>
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
expect(screen.getByRole('banner')).toBeInTheDocument(); // header
|
|
595
|
+
expect(screen.getByRole('main')).toBeInTheDocument(); // main
|
|
596
|
+
expect(screen.getByRole('contentinfo')).toBeInTheDocument(); // footer
|
|
597
|
+
|
|
598
|
+
const main = screen.getByTestId('mock-outlet').parentElement;
|
|
599
|
+
expect(main).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto', 'py-8');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('ensures main content area grows to fill available space', () => {
|
|
603
|
+
render(
|
|
604
|
+
<TestWrapper>
|
|
605
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
606
|
+
</TestWrapper>
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const main = screen.getByTestId('mock-outlet').parentElement;
|
|
610
|
+
expect(main).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto', 'py-8');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('integrates custom header className', () => {
|
|
614
|
+
render(
|
|
615
|
+
<TestWrapper>
|
|
616
|
+
<PaceAppLayout appName="Test App" headerClassName="custom-header-class" enforcePermissions={false} />
|
|
617
|
+
</TestWrapper>
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
expect(screen.getByTestId('mock-header')).toHaveClass('custom-header-class');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('integrates default header className when not provided', () => {
|
|
624
|
+
render(
|
|
625
|
+
<TestWrapper>
|
|
626
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
627
|
+
</TestWrapper>
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
expect(screen.getByTestId('mock-header')).toHaveClass('sticky', 'top-0', 'z-[40]', 'w-full');
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe('Real-world Scenarios', () => {
|
|
635
|
+
const stableComplexNavItems = [
|
|
636
|
+
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
|
|
637
|
+
{ id: 'reports', label: 'Reports', href: '/reports' },
|
|
638
|
+
{ id: 'admin', label: 'Admin', href: '/admin' }
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
it('handles complete user workflow', async () => {
|
|
642
|
+
render(
|
|
643
|
+
<TestWrapper>
|
|
644
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
645
|
+
</TestWrapper>
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// 1. User navigates to dashboard
|
|
649
|
+
fireEvent.click(screen.getByTestId('nav-dashboard'));
|
|
650
|
+
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
|
651
|
+
|
|
652
|
+
// 2. User changes password
|
|
653
|
+
fireEvent.click(screen.getByTestId('change-password-button'));
|
|
654
|
+
await waitFor(() => {
|
|
655
|
+
expect(mockUpdatePassword).toHaveBeenCalledWith('newpassword123');
|
|
656
|
+
}, { timeout: 5000 });
|
|
657
|
+
|
|
658
|
+
// 3. User navigates back to home
|
|
659
|
+
fireEvent.click(screen.getByTestId('nav-home'));
|
|
660
|
+
expect(mockNavigate).toHaveBeenCalledWith('/');
|
|
661
|
+
|
|
662
|
+
// 4. User signs out
|
|
663
|
+
fireEvent.click(screen.getByTestId('sign-out-button'));
|
|
664
|
+
await waitFor(() => {
|
|
665
|
+
expect(mockSignOut).toHaveBeenCalledTimes(1);
|
|
666
|
+
}, { timeout: 5000 });
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('handles rapid navigation and interactions', async () => {
|
|
670
|
+
render(
|
|
671
|
+
<TestWrapper>
|
|
672
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
673
|
+
</TestWrapper>
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Rapidly click navigation buttons
|
|
677
|
+
fireEvent.click(screen.getByTestId('nav-dashboard'));
|
|
678
|
+
fireEvent.click(screen.getByTestId('nav-home'));
|
|
679
|
+
fireEvent.click(screen.getByTestId('nav-settings'));
|
|
680
|
+
|
|
681
|
+
// Should have called navigate multiple times
|
|
682
|
+
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
|
683
|
+
expect(mockNavigate).toHaveBeenCalledWith('/');
|
|
684
|
+
expect(mockNavigate).toHaveBeenCalledWith('/settings');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('handles permission-based workflow', async () => {
|
|
688
|
+
// Mock permission check to simulate different permission states
|
|
689
|
+
mockIsPermitted.mockImplementation((input) => {
|
|
690
|
+
if (input.pageId === 'admin') return Promise.resolve(false);
|
|
691
|
+
return Promise.resolve(true);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const routePermissions = {
|
|
695
|
+
'/dashboard': 'read',
|
|
696
|
+
'/settings': 'read',
|
|
697
|
+
'/admin': 'read'
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
render(
|
|
701
|
+
<TestWrapper>
|
|
702
|
+
<PaceAppLayout
|
|
703
|
+
appName="Test App"
|
|
704
|
+
enforcePermissions={true}
|
|
705
|
+
routePermissions={routePermissions}
|
|
706
|
+
/>
|
|
707
|
+
</TestWrapper>
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Initially should check permissions
|
|
711
|
+
await waitFor(() => {
|
|
712
|
+
// With permission-based workflow, the component should render normally
|
|
713
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
714
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
715
|
+
}, { timeout: 2000 });
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('handles complex navigation with custom items and permissions', async () => {
|
|
719
|
+
// Mock permission check to filter admin access
|
|
720
|
+
mockIsPermitted.mockImplementation((input) => {
|
|
721
|
+
if (input.pageId === 'admin') return Promise.resolve(false);
|
|
722
|
+
return Promise.resolve(true);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const routePermissions = {
|
|
726
|
+
'/dashboard': 'read',
|
|
727
|
+
'/reports': 'read',
|
|
728
|
+
'/admin': 'read'
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
render(
|
|
732
|
+
<TestWrapper>
|
|
733
|
+
<PaceAppLayout
|
|
734
|
+
appName="Test App"
|
|
735
|
+
navItems={stableComplexNavItems}
|
|
736
|
+
enforcePermissions={true}
|
|
737
|
+
filterNavigationByPermissions={true}
|
|
738
|
+
routePermissions={routePermissions}
|
|
739
|
+
/>
|
|
740
|
+
</TestWrapper>
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
await waitFor(() => {
|
|
744
|
+
// With complex navigation and permissions, the component should render normally
|
|
745
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
746
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
747
|
+
}, { timeout: 2000 });
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('handles error recovery scenarios', async () => {
|
|
751
|
+
// Mock permission check to fail initially, then succeed
|
|
752
|
+
mockIsPermitted
|
|
753
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
754
|
+
.mockResolvedValue(true);
|
|
755
|
+
|
|
756
|
+
const { rerender } = render(
|
|
757
|
+
<TestWrapper>
|
|
758
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
759
|
+
</TestWrapper>
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
// Should show header normally since permission enforcement is disabled
|
|
763
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
764
|
+
|
|
765
|
+
// Clear mocks and re-render to simulate recovery
|
|
766
|
+
mockIsPermitted.mockClear();
|
|
767
|
+
mockIsPermitted.mockResolvedValue(true);
|
|
768
|
+
|
|
769
|
+
rerender(
|
|
770
|
+
<TestWrapper>
|
|
771
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
772
|
+
</TestWrapper>
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
// Should work normally after recovery
|
|
776
|
+
await waitFor(() => {
|
|
777
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
778
|
+
}, { timeout: 5000 });
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe('Configuration Integration', () => {
|
|
783
|
+
const stableConfigNavItems = [
|
|
784
|
+
{ id: 'custom1', label: 'Custom 1', href: '/custom1' },
|
|
785
|
+
{ id: 'custom2', label: 'Custom 2', href: '/custom2' }
|
|
786
|
+
];
|
|
787
|
+
|
|
788
|
+
it('integrates all configuration options together', () => {
|
|
789
|
+
|
|
790
|
+
const HeaderActions = () => <div data-testid="header-actions">Actions</div>;
|
|
791
|
+
const CustomUserMenu = () => <div data-testid="custom-user-menu">Menu</div>;
|
|
792
|
+
const CustomLogo = () => <div data-testid="custom-logo">Logo</div>;
|
|
793
|
+
|
|
794
|
+
const routePermissions = {
|
|
795
|
+
'/custom1': 'read',
|
|
796
|
+
'/custom2': 'update'
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const pageIdMapping = {
|
|
800
|
+
'/custom1': 'page-1',
|
|
801
|
+
'/custom2': 'page-2'
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
render(
|
|
805
|
+
<TestWrapper>
|
|
806
|
+
<PaceAppLayout
|
|
807
|
+
appName="Complex App"
|
|
808
|
+
navItems={stableConfigNavItems}
|
|
809
|
+
headerActions={<HeaderActions />}
|
|
810
|
+
customUserMenu={<CustomUserMenu />}
|
|
811
|
+
customLogo={<CustomLogo />}
|
|
812
|
+
showEventSelector={false}
|
|
813
|
+
showUserMenu={false}
|
|
814
|
+
headerClassName="complex-header-class"
|
|
815
|
+
enforcePermissions={false}
|
|
816
|
+
defaultPermission="update"
|
|
817
|
+
routePermissions={routePermissions}
|
|
818
|
+
pageIdMapping={pageIdMapping}
|
|
819
|
+
filterNavigationByPermissions={false}
|
|
820
|
+
/>
|
|
821
|
+
</TestWrapper>
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Verify all custom components are rendered
|
|
825
|
+
expect(screen.getByTestId('header-actions')).toBeInTheDocument();
|
|
826
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
827
|
+
expect(screen.getByTestId('custom-logo')).toBeInTheDocument();
|
|
828
|
+
|
|
829
|
+
// Verify configuration is applied
|
|
830
|
+
expect(screen.getByTestId('app-name')).toHaveTextContent('Complex App');
|
|
831
|
+
expect(screen.getByTestId('nav-items-count')).toHaveTextContent('2');
|
|
832
|
+
expect(screen.getByTestId('show-event-selector')).toHaveTextContent('false');
|
|
833
|
+
expect(screen.getByTestId('show-user-menu')).toHaveTextContent('false');
|
|
834
|
+
expect(screen.getByTestId('mock-header')).toHaveClass('complex-header-class');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('handles dynamic configuration changes', async () => {
|
|
838
|
+
const { rerender } = render(
|
|
839
|
+
<TestWrapper>
|
|
840
|
+
<PaceAppLayout appName="Test App" enforcePermissions={false} />
|
|
841
|
+
</TestWrapper>
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
// Initially should not have permission enforcement
|
|
845
|
+
expect(mockIsPermitted).not.toHaveBeenCalled();
|
|
846
|
+
|
|
847
|
+
// Enable permission enforcement
|
|
848
|
+
rerender(
|
|
849
|
+
<TestWrapper>
|
|
850
|
+
<PaceAppLayout appName="Test App" enforcePermissions={true} />
|
|
851
|
+
</TestWrapper>
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
await waitFor(() => {
|
|
855
|
+
// With dynamic configuration changes, the component should render normally
|
|
856
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
857
|
+
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
858
|
+
}, { timeout: 2000 });
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
});
|