@jmruthers/pace-core 0.5.1 → 0.5.4
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-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -55
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- 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 +96 -0
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
- 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 +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- 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 +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- 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 +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- 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 +85 -85
- 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 +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +952 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +364 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +551 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -55
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file InactivityWarningModal Component Tests
|
|
3
|
+
* @description Comprehensive test suite for InactivityWarningModal component
|
|
4
|
+
* @package @jmruthers/pace-core
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { InactivityWarningModal } from './InactivityWarningModal';
|
|
12
|
+
import { renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
13
|
+
|
|
14
|
+
// Mock the Dialog components to avoid complex modal behavior in tests
|
|
15
|
+
vi.mock('../Dialog/Dialog', () => ({
|
|
16
|
+
Dialog: ({ children, open, onOpenChange }: { children: React.ReactNode; open: boolean; onOpenChange: (open: boolean) => void }) =>
|
|
17
|
+
open ? <div data-testid="dialog" role="dialog">{children}</div> : null,
|
|
18
|
+
DialogContent: ({ children, className, preventCloseOnEscape, preventCloseOnOutsideClick, ...props }: any) => (
|
|
19
|
+
<div
|
|
20
|
+
data-testid="dialog-content"
|
|
21
|
+
className={className}
|
|
22
|
+
data-prevent-close-on-escape={preventCloseOnEscape}
|
|
23
|
+
data-prevent-close-on-outside-click={preventCloseOnOutsideClick}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
DialogHeader: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
30
|
+
<div data-testid="dialog-header" className={className} role="banner">
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
DialogTitle: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
35
|
+
<h2 data-testid="dialog-title" className={className} role="heading" aria-level={2}>
|
|
36
|
+
{children}
|
|
37
|
+
</h2>
|
|
38
|
+
),
|
|
39
|
+
DialogDescription: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
40
|
+
<p data-testid="dialog-description" className={className}>
|
|
41
|
+
{children}
|
|
42
|
+
</p>
|
|
43
|
+
),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock the Button component
|
|
47
|
+
vi.mock('../Button/Button', () => ({
|
|
48
|
+
Button: ({ children, onClick, className, size, variant, ...props }: any) => (
|
|
49
|
+
<button
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className={className}
|
|
52
|
+
data-size={size}
|
|
53
|
+
data-variant={variant}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</button>
|
|
58
|
+
),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock Lucide React icons
|
|
62
|
+
vi.mock('lucide-react', () => ({
|
|
63
|
+
Clock: () => <span data-testid="clock-icon">🕐</span>,
|
|
64
|
+
AlertTriangle: () => <span data-testid="alert-triangle-icon">⚠️</span>,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
describe('InactivityWarningModal Component', () => {
|
|
68
|
+
const defaultProps = {
|
|
69
|
+
isOpen: true,
|
|
70
|
+
timeRemaining: 45,
|
|
71
|
+
onStaySignedIn: vi.fn(),
|
|
72
|
+
onSignOutNow: vi.fn(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Rendering', () => {
|
|
80
|
+
it('renders when isOpen is true', () => {
|
|
81
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByTestId('dialog')).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByTestId('inactivity-warning-modal')).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not render when isOpen is false', () => {
|
|
88
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} isOpen={false} />);
|
|
89
|
+
|
|
90
|
+
expect(screen.queryByTestId('dialog')).not.toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders with default title and description', () => {
|
|
94
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('Session Timeout Warning')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByText("You've been inactive for a while. Your session will expire soon for security reasons.")).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders with custom title and description', () => {
|
|
101
|
+
const customProps = {
|
|
102
|
+
...defaultProps,
|
|
103
|
+
title: 'Custom Warning',
|
|
104
|
+
description: 'Custom description text',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
renderWithProviders(<InactivityWarningModal {...customProps} />);
|
|
108
|
+
|
|
109
|
+
expect(screen.getByText('Custom Warning')).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByText('Custom description text')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders with custom className', () => {
|
|
114
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} className="custom-modal" />);
|
|
115
|
+
|
|
116
|
+
const modal = screen.getByTestId('inactivity-warning-modal');
|
|
117
|
+
expect(modal).toHaveClass('custom-modal');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders countdown timer with correct time format', () => {
|
|
121
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={125} />);
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('02:05')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders countdown timer with single digit minutes', () => {
|
|
127
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={65} />);
|
|
128
|
+
|
|
129
|
+
expect(screen.getByText('01:05')).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('renders countdown timer with zero seconds', () => {
|
|
133
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={60} />);
|
|
134
|
+
|
|
135
|
+
expect(screen.getByText('01:00')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('renders countdown timer with single digit seconds', () => {
|
|
139
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={5} />);
|
|
140
|
+
|
|
141
|
+
expect(screen.getByText('00:05')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('renders countdown timer with zero time', () => {
|
|
145
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={0} />);
|
|
146
|
+
|
|
147
|
+
expect(screen.getByText('00:00')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders action buttons with correct text', () => {
|
|
151
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
152
|
+
|
|
153
|
+
expect(screen.getByRole('button', { name: 'Stay Signed In' })).toBeInTheDocument();
|
|
154
|
+
expect(screen.getByRole('button', { name: 'Sign Out Now' })).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renders icons correctly', () => {
|
|
158
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
159
|
+
|
|
160
|
+
expect(screen.getByTestId('alert-triangle-icon')).toBeInTheDocument();
|
|
161
|
+
expect(screen.getByTestId('clock-icon')).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('renders additional security information', () => {
|
|
165
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
166
|
+
|
|
167
|
+
expect(screen.getByText("For security reasons, you'll be automatically signed out after 30 minutes of inactivity.")).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Time Formatting', () => {
|
|
172
|
+
it('formats time correctly for various values', () => {
|
|
173
|
+
const testCases = [
|
|
174
|
+
{ input: 0, expected: '00:00' },
|
|
175
|
+
{ input: 5, expected: '00:05' },
|
|
176
|
+
{ input: 30, expected: '00:30' },
|
|
177
|
+
{ input: 60, expected: '01:00' },
|
|
178
|
+
{ input: 90, expected: '01:30' },
|
|
179
|
+
{ input: 125, expected: '02:05' },
|
|
180
|
+
{ input: 3661, expected: '61:01' },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
testCases.forEach(({ input, expected }) => {
|
|
184
|
+
const { unmount } = renderWithProviders(
|
|
185
|
+
<InactivityWarningModal {...defaultProps} timeRemaining={input} />
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(screen.getByText(expected)).toBeInTheDocument();
|
|
189
|
+
unmount();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('updates display time when timeRemaining prop changes', () => {
|
|
194
|
+
const { rerender } = renderWithProviders(
|
|
195
|
+
<InactivityWarningModal {...defaultProps} timeRemaining={60} />
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(screen.getByText('01:00')).toBeInTheDocument();
|
|
199
|
+
|
|
200
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={30} />);
|
|
201
|
+
|
|
202
|
+
expect(screen.getByText('00:30')).toBeInTheDocument();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Event Handling', () => {
|
|
207
|
+
it('calls onStaySignedIn when Stay Signed In button is clicked', async () => {
|
|
208
|
+
const user = userEvent.setup();
|
|
209
|
+
const onStaySignedIn = vi.fn();
|
|
210
|
+
|
|
211
|
+
renderWithProviders(
|
|
212
|
+
<InactivityWarningModal {...defaultProps} onStaySignedIn={onStaySignedIn} />
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await user.click(screen.getByRole('button', { name: 'Stay Signed In' }));
|
|
216
|
+
|
|
217
|
+
expect(onStaySignedIn).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('calls onSignOutNow when Sign Out Now button is clicked', async () => {
|
|
221
|
+
const user = userEvent.setup();
|
|
222
|
+
const onSignOutNow = vi.fn();
|
|
223
|
+
|
|
224
|
+
renderWithProviders(
|
|
225
|
+
<InactivityWarningModal {...defaultProps} onSignOutNow={onSignOutNow} />
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await user.click(screen.getByRole('button', { name: 'Sign Out Now' }));
|
|
229
|
+
|
|
230
|
+
expect(onSignOutNow).toHaveBeenCalledTimes(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('calls onStaySignedIn when dialog is closed via onOpenChange', async () => {
|
|
234
|
+
const user = userEvent.setup();
|
|
235
|
+
const onStaySignedIn = vi.fn();
|
|
236
|
+
|
|
237
|
+
renderWithProviders(
|
|
238
|
+
<InactivityWarningModal {...defaultProps} onStaySignedIn={onStaySignedIn} />
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Simulate dialog close (this would normally happen via the Dialog component)
|
|
242
|
+
const dialog = screen.getByTestId('dialog');
|
|
243
|
+
// The Dialog component would call onOpenChange(false) when closed
|
|
244
|
+
// We can't easily test this without more complex mocking, but we can test the behavior
|
|
245
|
+
expect(dialog).toBeInTheDocument();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('Button Styling and Props', () => {
|
|
250
|
+
it('applies correct styling to Stay Signed In button', () => {
|
|
251
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
252
|
+
|
|
253
|
+
const stayButton = screen.getByRole('button', { name: 'Stay Signed In' });
|
|
254
|
+
expect(stayButton).toHaveClass('bg-main-600', 'hover:bg-main-700', 'text-main-50');
|
|
255
|
+
expect(stayButton).toHaveAttribute('data-size', 'lg');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('applies correct styling to Sign Out Now button', () => {
|
|
259
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
260
|
+
|
|
261
|
+
const signOutButton = screen.getByRole('button', { name: 'Sign Out Now' });
|
|
262
|
+
expect(signOutButton).toHaveAttribute('data-variant', 'outline');
|
|
263
|
+
expect(signOutButton).toHaveClass('border-acc-300', 'text-acc-700', 'hover:bg-acc-50');
|
|
264
|
+
expect(signOutButton).toHaveAttribute('data-size', 'lg');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Accessibility', () => {
|
|
269
|
+
it('has proper ARIA attributes', () => {
|
|
270
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
271
|
+
|
|
272
|
+
const dialog = screen.getByTestId('dialog');
|
|
273
|
+
expect(dialog).toHaveAttribute('role', 'dialog');
|
|
274
|
+
|
|
275
|
+
const title = screen.getByTestId('dialog-title');
|
|
276
|
+
expect(title).toHaveAttribute('role', 'heading');
|
|
277
|
+
expect(title).toHaveAttribute('aria-level', '2');
|
|
278
|
+
|
|
279
|
+
const header = screen.getByTestId('dialog-header');
|
|
280
|
+
expect(header).toHaveAttribute('role', 'banner');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('has accessible button labels', () => {
|
|
284
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
285
|
+
|
|
286
|
+
expect(screen.getByRole('button', { name: 'Stay Signed In' })).toBeInTheDocument();
|
|
287
|
+
expect(screen.getByRole('button', { name: 'Sign Out Now' })).toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('has proper heading structure', () => {
|
|
291
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
292
|
+
|
|
293
|
+
const title = screen.getByRole('heading', { level: 2 });
|
|
294
|
+
expect(title).toBeInTheDocument();
|
|
295
|
+
expect(title).toHaveTextContent('Session Timeout Warning');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('provides descriptive text for screen readers', () => {
|
|
299
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
300
|
+
|
|
301
|
+
expect(screen.getByText('Time remaining before automatic logout')).toBeInTheDocument();
|
|
302
|
+
expect(screen.getByText("For security reasons, you'll be automatically signed out after 30 minutes of inactivity.")).toBeInTheDocument();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Dialog Configuration', () => {
|
|
307
|
+
it('configures dialog with correct props', () => {
|
|
308
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} />);
|
|
309
|
+
|
|
310
|
+
const modal = screen.getByTestId('inactivity-warning-modal');
|
|
311
|
+
expect(modal).toHaveClass('sm:max-w-md');
|
|
312
|
+
expect(modal).toHaveAttribute('data-prevent-close-on-escape', 'false');
|
|
313
|
+
expect(modal).toHaveAttribute('data-prevent-close-on-outside-click', 'true');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('applies custom className to dialog content', () => {
|
|
317
|
+
renderWithProviders(
|
|
318
|
+
<InactivityWarningModal {...defaultProps} className="custom-dialog-class" />
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const modal = screen.getByTestId('inactivity-warning-modal');
|
|
322
|
+
expect(modal).toHaveClass('custom-dialog-class');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Edge Cases', () => {
|
|
327
|
+
it('handles negative time values gracefully', () => {
|
|
328
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={-5} />);
|
|
329
|
+
|
|
330
|
+
// Should still render, but with negative time formatted as the component actually does it
|
|
331
|
+
expect(screen.getByText('-1:-5')).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('handles very large time values', () => {
|
|
335
|
+
renderWithProviders(<InactivityWarningModal {...defaultProps} timeRemaining={999999} />);
|
|
336
|
+
|
|
337
|
+
expect(screen.getByText('16666:39')).toBeInTheDocument();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('handles undefined title and description', () => {
|
|
341
|
+
const propsWithoutDefaults = {
|
|
342
|
+
isOpen: true,
|
|
343
|
+
timeRemaining: 30,
|
|
344
|
+
onStaySignedIn: vi.fn(),
|
|
345
|
+
onSignOutNow: vi.fn(),
|
|
346
|
+
title: undefined,
|
|
347
|
+
description: undefined,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
renderWithProviders(<InactivityWarningModal {...propsWithoutDefaults} />);
|
|
351
|
+
|
|
352
|
+
// Should use default values
|
|
353
|
+
expect(screen.getByText('Session Timeout Warning')).toBeInTheDocument();
|
|
354
|
+
expect(screen.getByText("You've been inactive for a while. Your session will expire soon for security reasons.")).toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('handles empty string title and description', () => {
|
|
358
|
+
renderWithProviders(
|
|
359
|
+
<InactivityWarningModal
|
|
360
|
+
{...defaultProps}
|
|
361
|
+
title=""
|
|
362
|
+
description=""
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Should render empty strings
|
|
367
|
+
const title = screen.getByTestId('dialog-title');
|
|
368
|
+
const description = screen.getByTestId('dialog-description');
|
|
369
|
+
expect(title).toHaveTextContent('');
|
|
370
|
+
expect(description).toHaveTextContent('');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Integration', () => {
|
|
375
|
+
it('works with rapid prop changes', () => {
|
|
376
|
+
const { rerender } = renderWithProviders(
|
|
377
|
+
<InactivityWarningModal {...defaultProps} timeRemaining={60} />
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(screen.getByText('01:00')).toBeInTheDocument();
|
|
381
|
+
|
|
382
|
+
// Rapid changes
|
|
383
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={30} />);
|
|
384
|
+
expect(screen.getByText('00:30')).toBeInTheDocument();
|
|
385
|
+
|
|
386
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={15} />);
|
|
387
|
+
expect(screen.getByText('00:15')).toBeInTheDocument();
|
|
388
|
+
|
|
389
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={0} />);
|
|
390
|
+
expect(screen.getByText('00:00')).toBeInTheDocument();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('handles multiple rapid button clicks', async () => {
|
|
394
|
+
const user = userEvent.setup();
|
|
395
|
+
const onStaySignedIn = vi.fn();
|
|
396
|
+
const onSignOutNow = vi.fn();
|
|
397
|
+
|
|
398
|
+
renderWithProviders(
|
|
399
|
+
<InactivityWarningModal
|
|
400
|
+
{...defaultProps}
|
|
401
|
+
onStaySignedIn={onStaySignedIn}
|
|
402
|
+
onSignOutNow={onSignOutNow}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const stayButton = screen.getByRole('button', { name: 'Stay Signed In' });
|
|
407
|
+
const signOutButton = screen.getByRole('button', { name: 'Sign Out Now' });
|
|
408
|
+
|
|
409
|
+
// Rapid clicks on both buttons
|
|
410
|
+
await user.click(stayButton);
|
|
411
|
+
await user.click(signOutButton);
|
|
412
|
+
await user.click(stayButton);
|
|
413
|
+
|
|
414
|
+
expect(onStaySignedIn).toHaveBeenCalledTimes(2);
|
|
415
|
+
expect(onSignOutNow).toHaveBeenCalledTimes(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('maintains state consistency during re-renders', () => {
|
|
419
|
+
const { rerender } = renderWithProviders(
|
|
420
|
+
<InactivityWarningModal {...defaultProps} timeRemaining={45} />
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(screen.getByText('00:45')).toBeInTheDocument();
|
|
424
|
+
|
|
425
|
+
// Re-render with same props
|
|
426
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={45} />);
|
|
427
|
+
expect(screen.getByText('00:45')).toBeInTheDocument();
|
|
428
|
+
|
|
429
|
+
// Re-render with different props
|
|
430
|
+
rerender(<InactivityWarningModal {...defaultProps} timeRemaining={30} />);
|
|
431
|
+
expect(screen.getByText('00:30')).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('Error Handling', () => {
|
|
436
|
+
it('handles missing callback functions gracefully', () => {
|
|
437
|
+
const propsWithoutCallbacks = {
|
|
438
|
+
isOpen: true,
|
|
439
|
+
timeRemaining: 30,
|
|
440
|
+
onStaySignedIn: undefined as any,
|
|
441
|
+
onSignOutNow: undefined as any,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
renderWithProviders(<InactivityWarningModal {...propsWithoutCallbacks} />);
|
|
445
|
+
|
|
446
|
+
// Should render without errors
|
|
447
|
+
expect(screen.getByTestId('dialog')).toBeInTheDocument();
|
|
448
|
+
expect(screen.getByRole('button', { name: 'Stay Signed In' })).toBeInTheDocument();
|
|
449
|
+
expect(screen.getByRole('button', { name: 'Sign Out Now' })).toBeInTheDocument();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('handles invalid timeRemaining values', () => {
|
|
453
|
+
const invalidTimeProps = {
|
|
454
|
+
...defaultProps,
|
|
455
|
+
timeRemaining: NaN,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
renderWithProviders(<InactivityWarningModal {...invalidTimeProps} />);
|
|
459
|
+
|
|
460
|
+
// Should render with NaN formatted as "NaN:NaN" or handle gracefully
|
|
461
|
+
expect(screen.getByTestId('dialog')).toBeInTheDocument();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('Performance', () => {
|
|
466
|
+
it('does not cause unnecessary re-renders with stable props', () => {
|
|
467
|
+
const stableProps = {
|
|
468
|
+
isOpen: true,
|
|
469
|
+
timeRemaining: 30,
|
|
470
|
+
onStaySignedIn: vi.fn(),
|
|
471
|
+
onSignOutNow: vi.fn(),
|
|
472
|
+
title: 'Test Title',
|
|
473
|
+
description: 'Test Description',
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const { rerender } = renderWithProviders(
|
|
477
|
+
<InactivityWarningModal {...stableProps} />
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const initialTime = screen.getByText('00:30');
|
|
481
|
+
|
|
482
|
+
// Re-render with same props
|
|
483
|
+
rerender(<InactivityWarningModal {...stableProps} />);
|
|
484
|
+
|
|
485
|
+
// Should still show the same time
|
|
486
|
+
expect(screen.getByText('00:30')).toBeInTheDocument();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|