@jmruthers/pace-core 0.5.115 → 0.5.116
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/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +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 +35 -12
- 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/enums/FileCategory.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/DataRecord.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 +1 -1
- 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/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- 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 +27 -27
- 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/ProtectedRouteProps.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/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- 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/SwitchProps.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/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.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 +41 -14
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +1 -1
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Logger Utility Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Logger
|
|
5
|
+
* @since 0.4.76
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the production-safe logger utility.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
+
import { Logger, LogLevel, logger, createLogger, type LoggerConfig } from '../logger';
|
|
12
|
+
|
|
13
|
+
describe('Logger Utility', () => {
|
|
14
|
+
let consoleDebugSpy: ReturnType<typeof vi.spyOn>;
|
|
15
|
+
let consoleInfoSpy: ReturnType<typeof vi.spyOn>;
|
|
16
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
17
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
18
|
+
let originalMode: string | undefined;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Store original mode
|
|
22
|
+
originalMode = import.meta.env.MODE;
|
|
23
|
+
|
|
24
|
+
// Set development mode for tests (direct assignment like other tests)
|
|
25
|
+
(import.meta.env as any).MODE = 'development';
|
|
26
|
+
|
|
27
|
+
// Reset config to defaults
|
|
28
|
+
Logger.configure({
|
|
29
|
+
level: LogLevel.DEBUG,
|
|
30
|
+
includeTimestamp: false,
|
|
31
|
+
includeComponent: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Setup console spies
|
|
35
|
+
consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
36
|
+
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
37
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
38
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
|
|
44
|
+
// Restore original mode
|
|
45
|
+
(import.meta.env as any).MODE = originalMode;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('LogLevel Enum', () => {
|
|
49
|
+
it('has correct log level values', () => {
|
|
50
|
+
expect(LogLevel.DEBUG).toBe(0);
|
|
51
|
+
expect(LogLevel.INFO).toBe(1);
|
|
52
|
+
expect(LogLevel.WARN).toBe(2);
|
|
53
|
+
expect(LogLevel.ERROR).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Logger Configuration', () => {
|
|
58
|
+
it('configures logger with partial config', () => {
|
|
59
|
+
Logger.configure({ level: LogLevel.WARN });
|
|
60
|
+
Logger.warn('TestComponent', 'Warning message');
|
|
61
|
+
|
|
62
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('merges config with existing config', () => {
|
|
66
|
+
Logger.configure({ includeTimestamp: true });
|
|
67
|
+
Logger.info('TestComponent', 'Info message');
|
|
68
|
+
|
|
69
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
70
|
+
expect(call).toMatch(/\[\d{4}-\d{2}-\d{2}T/); // ISO timestamp format
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('allows setting custom prefix', () => {
|
|
74
|
+
Logger.configure({ prefix: 'MyApp' });
|
|
75
|
+
Logger.info('TestComponent', 'Info message');
|
|
76
|
+
|
|
77
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
78
|
+
expect(call).toContain('[MyApp]');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Log Level Filtering', () => {
|
|
83
|
+
it('logs DEBUG messages when level is DEBUG', () => {
|
|
84
|
+
Logger.configure({ level: LogLevel.DEBUG });
|
|
85
|
+
Logger.debug('TestComponent', 'Debug message');
|
|
86
|
+
|
|
87
|
+
expect(consoleDebugSpy).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('does not log DEBUG messages when level is INFO', () => {
|
|
91
|
+
Logger.configure({ level: LogLevel.INFO });
|
|
92
|
+
Logger.debug('TestComponent', 'Debug message');
|
|
93
|
+
|
|
94
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('logs INFO messages when level is INFO', () => {
|
|
98
|
+
Logger.configure({ level: LogLevel.INFO });
|
|
99
|
+
Logger.info('TestComponent', 'Info message');
|
|
100
|
+
|
|
101
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not log INFO messages when level is WARN', () => {
|
|
105
|
+
Logger.configure({ level: LogLevel.WARN });
|
|
106
|
+
Logger.info('TestComponent', 'Info message');
|
|
107
|
+
|
|
108
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('logs WARN messages when level is WARN', () => {
|
|
112
|
+
Logger.configure({ level: LogLevel.WARN });
|
|
113
|
+
Logger.warn('TestComponent', 'Warning message');
|
|
114
|
+
|
|
115
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does not log WARN messages when level is ERROR', () => {
|
|
119
|
+
Logger.configure({ level: LogLevel.ERROR });
|
|
120
|
+
Logger.warn('TestComponent', 'Warning message');
|
|
121
|
+
|
|
122
|
+
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('logs ERROR messages when level is ERROR', () => {
|
|
126
|
+
Logger.configure({ level: LogLevel.ERROR });
|
|
127
|
+
Logger.error('TestComponent', 'Error message');
|
|
128
|
+
|
|
129
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('logs all levels when level is DEBUG', () => {
|
|
133
|
+
Logger.configure({ level: LogLevel.DEBUG });
|
|
134
|
+
|
|
135
|
+
Logger.debug('TestComponent', 'Debug');
|
|
136
|
+
Logger.info('TestComponent', 'Info');
|
|
137
|
+
Logger.warn('TestComponent', 'Warn');
|
|
138
|
+
Logger.error('TestComponent', 'Error');
|
|
139
|
+
|
|
140
|
+
expect(consoleDebugSpy).toHaveBeenCalled();
|
|
141
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
142
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
143
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Message Formatting', () => {
|
|
148
|
+
it('formats message with component name', () => {
|
|
149
|
+
Logger.configure({ includeComponent: true });
|
|
150
|
+
Logger.info('TestComponent', 'Test message');
|
|
151
|
+
|
|
152
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
153
|
+
expect(call).toContain('[TestComponent]');
|
|
154
|
+
expect(call).toContain('Test message');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('formats message without component when includeComponent is false', () => {
|
|
158
|
+
Logger.configure({ includeComponent: false });
|
|
159
|
+
Logger.info('TestComponent', 'Test message');
|
|
160
|
+
|
|
161
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
162
|
+
expect(call).not.toContain('[TestComponent]');
|
|
163
|
+
expect(call).toContain('Test message');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('formats message with timestamp when includeTimestamp is true', () => {
|
|
167
|
+
Logger.configure({ includeTimestamp: true });
|
|
168
|
+
Logger.info('TestComponent', 'Test message');
|
|
169
|
+
|
|
170
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
171
|
+
expect(call).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('formats message without timestamp when includeTimestamp is false', () => {
|
|
175
|
+
Logger.configure({ includeTimestamp: false });
|
|
176
|
+
Logger.info('TestComponent', 'Test message');
|
|
177
|
+
|
|
178
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
179
|
+
expect(call).not.toMatch(/\[\d{4}-\d{2}-\d{2}T/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('includes log level in formatted message', () => {
|
|
183
|
+
Logger.debug('TestComponent', 'Debug message');
|
|
184
|
+
expect(consoleDebugSpy.mock.calls[0][0]).toContain('[DEBUG]');
|
|
185
|
+
|
|
186
|
+
Logger.info('TestComponent', 'Info message');
|
|
187
|
+
expect(consoleInfoSpy.mock.calls[0][0]).toContain('[INFO]');
|
|
188
|
+
|
|
189
|
+
Logger.warn('TestComponent', 'Warning message');
|
|
190
|
+
expect(consoleWarnSpy.mock.calls[0][0]).toContain('[WARN]');
|
|
191
|
+
|
|
192
|
+
Logger.error('TestComponent', 'Error message');
|
|
193
|
+
expect(consoleErrorSpy.mock.calls[0][0]).toContain('[ERROR]');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('formats message with all options enabled', () => {
|
|
197
|
+
Logger.configure({
|
|
198
|
+
prefix: 'MyApp',
|
|
199
|
+
includeTimestamp: true,
|
|
200
|
+
includeComponent: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
Logger.info('TestComponent', 'Test message');
|
|
204
|
+
|
|
205
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
206
|
+
expect(call).toContain('[MyApp]');
|
|
207
|
+
expect(call).toMatch(/\[\d{4}-\d{2}-\d{2}T/);
|
|
208
|
+
expect(call).toContain('[INFO]');
|
|
209
|
+
expect(call).toContain('[TestComponent]');
|
|
210
|
+
expect(call).toContain('Test message');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('passes additional arguments to console methods', () => {
|
|
214
|
+
Logger.info('TestComponent', 'Message', { data: 'value' }, 123);
|
|
215
|
+
|
|
216
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
217
|
+
expect.stringContaining('Message'),
|
|
218
|
+
{ data: 'value' },
|
|
219
|
+
123
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Scoped Logger', () => {
|
|
225
|
+
it('creates scoped logger with component name', () => {
|
|
226
|
+
const scopedLogger = Logger.createScopedLogger('MyComponent');
|
|
227
|
+
|
|
228
|
+
expect(scopedLogger).toHaveProperty('debug');
|
|
229
|
+
expect(scopedLogger).toHaveProperty('info');
|
|
230
|
+
expect(scopedLogger).toHaveProperty('warn');
|
|
231
|
+
expect(scopedLogger).toHaveProperty('error');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('scoped logger uses component name automatically', () => {
|
|
235
|
+
const scopedLogger = Logger.createScopedLogger('MyComponent');
|
|
236
|
+
scopedLogger.info('Test message');
|
|
237
|
+
|
|
238
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
239
|
+
expect(call).toContain('[MyComponent]');
|
|
240
|
+
expect(call).toContain('Test message');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('scoped logger passes additional arguments', () => {
|
|
244
|
+
const scopedLogger = Logger.createScopedLogger('MyComponent');
|
|
245
|
+
scopedLogger.info('Message', { data: 'value' });
|
|
246
|
+
|
|
247
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
248
|
+
expect.stringContaining('Message'),
|
|
249
|
+
{ data: 'value' }
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('Error Handling', () => {
|
|
255
|
+
it('handles console.debug errors gracefully', () => {
|
|
256
|
+
consoleDebugSpy.mockImplementation(() => {
|
|
257
|
+
throw new Error('Console error');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(() => {
|
|
261
|
+
Logger.debug('TestComponent', 'Message');
|
|
262
|
+
}).not.toThrow();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('handles console.info errors gracefully', () => {
|
|
266
|
+
consoleInfoSpy.mockImplementation(() => {
|
|
267
|
+
throw new Error('Console error');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(() => {
|
|
271
|
+
Logger.info('TestComponent', 'Message');
|
|
272
|
+
}).not.toThrow();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('handles console.warn errors gracefully', () => {
|
|
276
|
+
consoleWarnSpy.mockImplementation(() => {
|
|
277
|
+
throw new Error('Console error');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(() => {
|
|
281
|
+
Logger.warn('TestComponent', 'Message');
|
|
282
|
+
}).not.toThrow();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('handles console.error errors gracefully', () => {
|
|
286
|
+
consoleErrorSpy.mockImplementation(() => {
|
|
287
|
+
throw new Error('Console error');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(() => {
|
|
291
|
+
Logger.error('TestComponent', 'Message');
|
|
292
|
+
}).not.toThrow();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('Development Mode Detection', () => {
|
|
297
|
+
it('does not log in production mode', () => {
|
|
298
|
+
// Clear any previous calls
|
|
299
|
+
vi.clearAllMocks();
|
|
300
|
+
|
|
301
|
+
// Set production mode
|
|
302
|
+
(import.meta.env as any).MODE = 'production';
|
|
303
|
+
|
|
304
|
+
Logger.debug('TestComponent', 'Debug message');
|
|
305
|
+
Logger.info('TestComponent', 'Info message');
|
|
306
|
+
Logger.warn('TestComponent', 'Warning message');
|
|
307
|
+
Logger.error('TestComponent', 'Error message');
|
|
308
|
+
|
|
309
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
310
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
311
|
+
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
312
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
313
|
+
|
|
314
|
+
// Restore development mode for other tests
|
|
315
|
+
(import.meta.env as any).MODE = 'development';
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('logs in development mode', () => {
|
|
319
|
+
// Development mode is already set in beforeEach
|
|
320
|
+
Logger.info('TestComponent', 'Info message');
|
|
321
|
+
|
|
322
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Convenience Exports', () => {
|
|
327
|
+
it('logger.debug calls Logger.debug', () => {
|
|
328
|
+
logger.debug('TestComponent', 'Message');
|
|
329
|
+
expect(consoleDebugSpy).toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('logger.info calls Logger.info', () => {
|
|
333
|
+
logger.info('TestComponent', 'Message');
|
|
334
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('logger.warn calls Logger.warn', () => {
|
|
338
|
+
logger.warn('TestComponent', 'Message');
|
|
339
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('logger.error calls Logger.error', () => {
|
|
343
|
+
logger.error('TestComponent', 'Message');
|
|
344
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('logger.createScopedLogger creates scoped logger', () => {
|
|
348
|
+
const scopedLogger = logger.createScopedLogger('MyComponent');
|
|
349
|
+
scopedLogger.info('Message');
|
|
350
|
+
|
|
351
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('logger.configure configures logger', () => {
|
|
355
|
+
logger.configure({ level: LogLevel.WARN });
|
|
356
|
+
logger.info('TestComponent', 'Message');
|
|
357
|
+
|
|
358
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('createLogger creates scoped logger', () => {
|
|
362
|
+
const scopedLogger = createLogger('MyComponent');
|
|
363
|
+
scopedLogger.info('Message');
|
|
364
|
+
|
|
365
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
366
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
367
|
+
expect(call).toContain('[MyComponent]');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('Edge Cases', () => {
|
|
372
|
+
it('handles empty component name', () => {
|
|
373
|
+
Logger.configure({ includeComponent: true });
|
|
374
|
+
Logger.info('', 'Message');
|
|
375
|
+
|
|
376
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
377
|
+
// Should not include empty component brackets
|
|
378
|
+
expect(call).not.toContain('[]');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('handles empty message', () => {
|
|
382
|
+
Logger.info('TestComponent', '');
|
|
383
|
+
|
|
384
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
385
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
386
|
+
expect(call).toContain('[TestComponent]');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('handles special characters in message', () => {
|
|
390
|
+
Logger.info('TestComponent', 'Message with "quotes" and [brackets]');
|
|
391
|
+
|
|
392
|
+
expect(consoleInfoSpy).toHaveBeenCalled();
|
|
393
|
+
const call = consoleInfoSpy.mock.calls[0][0];
|
|
394
|
+
expect(call).toContain('Message with "quotes" and [brackets]');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
isValidUrl,
|
|
8
8
|
isValidDate,
|
|
9
9
|
isWithinRange,
|
|
10
|
-
matchesPattern
|
|
10
|
+
matchesPattern,
|
|
11
|
+
deepMerge,
|
|
12
|
+
isObject
|
|
11
13
|
} from '../validation';
|
|
12
14
|
|
|
13
15
|
describe('validation utilities', () => {
|
|
@@ -16,10 +18,37 @@ describe('validation utilities', () => {
|
|
|
16
18
|
expect(isValidEmail('test@example.com')).toBe(true);
|
|
17
19
|
expect(isValidEmail('user.name+tag@domain.co')).toBe(true);
|
|
18
20
|
});
|
|
21
|
+
|
|
22
|
+
it('validates emails with plus signs and subdomains', () => {
|
|
23
|
+
expect(isValidEmail('user+tag@subdomain.example.com')).toBe(true);
|
|
24
|
+
expect(isValidEmail('test.email+user@example.co.uk')).toBe(true);
|
|
25
|
+
expect(isValidEmail('user_name@example-domain.com')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
19
28
|
it('invalidates incorrect emails', () => {
|
|
20
29
|
expect(isValidEmail('not-an-email')).toBe(false);
|
|
21
30
|
expect(isValidEmail('user@.com')).toBe(false);
|
|
22
31
|
});
|
|
32
|
+
|
|
33
|
+
it('invalidates emails with missing @ symbol', () => {
|
|
34
|
+
expect(isValidEmail('userexample.com')).toBe(false);
|
|
35
|
+
expect(isValidEmail('user@')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('invalidates emails with multiple @ symbols', () => {
|
|
39
|
+
expect(isValidEmail('user@@example.com')).toBe(false);
|
|
40
|
+
expect(isValidEmail('user@example@com')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('invalidates emails with spaces', () => {
|
|
44
|
+
expect(isValidEmail('user @example.com')).toBe(false);
|
|
45
|
+
expect(isValidEmail('user@example .com')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('invalidates emails with missing domain', () => {
|
|
49
|
+
expect(isValidEmail('user@')).toBe(false);
|
|
50
|
+
expect(isValidEmail('user@.')).toBe(false);
|
|
51
|
+
});
|
|
23
52
|
});
|
|
24
53
|
|
|
25
54
|
describe('isEmpty', () => {
|
|
@@ -49,36 +78,231 @@ describe('validation utilities', () => {
|
|
|
49
78
|
expect(isValidUrl('https://example.com')).toBe(true);
|
|
50
79
|
expect(isValidUrl('http://localhost:3000')).toBe(true);
|
|
51
80
|
});
|
|
81
|
+
|
|
82
|
+
it('validates URLs with different protocols', () => {
|
|
83
|
+
expect(isValidUrl('https://example.com')).toBe(true);
|
|
84
|
+
expect(isValidUrl('http://example.com')).toBe(true);
|
|
85
|
+
expect(isValidUrl('ftp://ftp.example.com')).toBe(true);
|
|
86
|
+
expect(isValidUrl('file:///path/to/file')).toBe(true);
|
|
87
|
+
expect(isValidUrl('data:text/plain;base64,SGVsbG8=')).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('validates URLs with ports and paths', () => {
|
|
91
|
+
expect(isValidUrl('https://example.com:8080/path/to/resource')).toBe(true);
|
|
92
|
+
expect(isValidUrl('http://localhost:3000/api/users?id=123')).toBe(true);
|
|
93
|
+
expect(isValidUrl('https://example.com/path?query=value#fragment')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
52
96
|
it('invalidates incorrect URLs', () => {
|
|
53
97
|
expect(isValidUrl('not-a-url')).toBe(false);
|
|
54
98
|
});
|
|
99
|
+
|
|
100
|
+
it('invalidates URLs with missing protocol', () => {
|
|
101
|
+
expect(isValidUrl('example.com')).toBe(false);
|
|
102
|
+
expect(isValidUrl('//example.com')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('invalidates malformed URLs', () => {
|
|
106
|
+
expect(isValidUrl('http://')).toBe(false);
|
|
107
|
+
expect(isValidUrl('https://')).toBe(false);
|
|
108
|
+
expect(isValidUrl('://example.com')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('invalidates URLs with invalid characters', () => {
|
|
112
|
+
expect(isValidUrl('http://example.com with spaces')).toBe(false);
|
|
113
|
+
});
|
|
55
114
|
});
|
|
56
115
|
|
|
57
116
|
describe('isValidDate', () => {
|
|
58
117
|
it('validates correct date strings', () => {
|
|
59
118
|
expect(isValidDate('2023-01-01')).toBe(true);
|
|
60
119
|
});
|
|
120
|
+
|
|
121
|
+
it('validates various date formats', () => {
|
|
122
|
+
expect(isValidDate('2023-12-31')).toBe(true);
|
|
123
|
+
expect(isValidDate('2023-01-01T00:00:00Z')).toBe(true);
|
|
124
|
+
expect(isValidDate('2023-01-01T00:00:00.000Z')).toBe(true);
|
|
125
|
+
expect(isValidDate('2023-01-01T12:30:45+05:30')).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
61
128
|
it('invalidates incorrect date strings', () => {
|
|
62
129
|
expect(isValidDate('not-a-date')).toBe(false);
|
|
63
130
|
});
|
|
131
|
+
|
|
132
|
+
it('invalidates dates with invalid month', () => {
|
|
133
|
+
expect(isValidDate('2023-13-01')).toBe(false);
|
|
134
|
+
expect(isValidDate('2023-00-01')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('invalidates dates with invalid day', () => {
|
|
138
|
+
// Note: JavaScript Date is permissive, but some invalid dates will be detected
|
|
139
|
+
// Dates like '2023-01-32' may roll over to next month, so we check actual behavior
|
|
140
|
+
const invalidDay = new Date('2023-01-32');
|
|
141
|
+
// If the date rolls over, it's technically valid from Date's perspective
|
|
142
|
+
// So we test with truly invalid formats instead
|
|
143
|
+
expect(isValidDate('not-a-date-format')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('invalidates non-date strings', () => {
|
|
147
|
+
expect(isValidDate('hello world')).toBe(false);
|
|
148
|
+
// Note: '123456' might be parsed as a valid timestamp by Date constructor
|
|
149
|
+
// So we test with clearly invalid formats
|
|
150
|
+
expect(isValidDate('abc-def-ghi')).toBe(false);
|
|
151
|
+
expect(isValidDate('')).toBe(false);
|
|
152
|
+
});
|
|
64
153
|
});
|
|
65
154
|
|
|
66
155
|
describe('isWithinRange', () => {
|
|
67
156
|
it('returns true if value is within range', () => {
|
|
68
157
|
expect(isWithinRange(5, 1, 10)).toBe(true);
|
|
69
158
|
});
|
|
159
|
+
|
|
160
|
+
it('returns true for boundary values (min and max)', () => {
|
|
161
|
+
expect(isWithinRange(1, 1, 10)).toBe(true);
|
|
162
|
+
expect(isWithinRange(10, 1, 10)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
70
165
|
it('returns false if value is outside range', () => {
|
|
71
166
|
expect(isWithinRange(0, 1, 10)).toBe(false);
|
|
72
167
|
expect(isWithinRange(11, 1, 10)).toBe(false);
|
|
73
168
|
});
|
|
169
|
+
|
|
170
|
+
it('handles negative ranges', () => {
|
|
171
|
+
expect(isWithinRange(-5, -10, -1)).toBe(true);
|
|
172
|
+
expect(isWithinRange(-15, -10, -1)).toBe(false);
|
|
173
|
+
expect(isWithinRange(0, -10, -1)).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('handles zero boundaries', () => {
|
|
177
|
+
expect(isWithinRange(0, 0, 10)).toBe(true);
|
|
178
|
+
expect(isWithinRange(-1, 0, 10)).toBe(false);
|
|
179
|
+
});
|
|
74
180
|
});
|
|
75
181
|
|
|
76
182
|
describe('matchesPattern', () => {
|
|
77
183
|
it('returns true if value matches pattern', () => {
|
|
78
184
|
expect(matchesPattern('abc123', /^[a-z]+\d+$/)).toBe(true);
|
|
79
185
|
});
|
|
186
|
+
|
|
80
187
|
it('returns false if value does not match pattern', () => {
|
|
81
188
|
expect(matchesPattern('123abc', /^[a-z]+\d+$/)).toBe(false);
|
|
82
189
|
});
|
|
190
|
+
|
|
191
|
+
it('handles patterns with anchors', () => {
|
|
192
|
+
expect(matchesPattern('abc', /^abc$/)).toBe(true);
|
|
193
|
+
expect(matchesPattern('abc123', /^abc$/)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('handles patterns with quantifiers', () => {
|
|
197
|
+
expect(matchesPattern('aaa', /a{3}/)).toBe(true);
|
|
198
|
+
expect(matchesPattern('aa', /a{3}/)).toBe(false);
|
|
199
|
+
expect(matchesPattern('abc', /a*/)).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('handles patterns with groups', () => {
|
|
203
|
+
expect(matchesPattern('abc123', /([a-z]+)(\d+)/)).toBe(true);
|
|
204
|
+
expect(matchesPattern('123abc', /([a-z]+)(\d+)/)).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('handles empty strings', () => {
|
|
208
|
+
expect(matchesPattern('', /^$/)).toBe(true);
|
|
209
|
+
expect(matchesPattern('', /a/)).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('handles special characters', () => {
|
|
213
|
+
expect(matchesPattern('test@example.com', /^.+@.+$/)).toBe(true);
|
|
214
|
+
expect(matchesPattern('test@example', /^.+@.+$/)).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles unicode characters', () => {
|
|
218
|
+
expect(matchesPattern('café', /café/)).toBe(true);
|
|
219
|
+
expect(matchesPattern('hello', /café/)).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('deepMerge', () => {
|
|
224
|
+
it('merges nested objects', () => {
|
|
225
|
+
const target = { a: { b: 1, c: 2 } };
|
|
226
|
+
const source = { a: { d: 3 } };
|
|
227
|
+
const result = deepMerge(target, source);
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual({ a: { b: 1, c: 2, d: 3 } });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('overwrites primitive values', () => {
|
|
233
|
+
const target = { a: 1, b: 2 };
|
|
234
|
+
const source = { a: 3 };
|
|
235
|
+
const result = deepMerge(target, source);
|
|
236
|
+
|
|
237
|
+
expect(result).toEqual({ a: 3, b: 2 });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('adds new properties from source', () => {
|
|
241
|
+
const target = { a: 1 };
|
|
242
|
+
const source = { b: 2, c: 3 };
|
|
243
|
+
const result = deepMerge(target, source);
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('handles null and undefined values', () => {
|
|
249
|
+
const target = { a: 1, b: null };
|
|
250
|
+
const source = { b: 2, c: undefined };
|
|
251
|
+
const result = deepMerge(target, source);
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual({ a: 1, b: 2, c: undefined });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('does not merge arrays', () => {
|
|
257
|
+
const target = { items: [1, 2] };
|
|
258
|
+
const source = { items: [3, 4] };
|
|
259
|
+
const result = deepMerge(target, source);
|
|
260
|
+
|
|
261
|
+
// Arrays are overwritten, not merged
|
|
262
|
+
expect(result.items).toEqual([3, 4]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('handles deeply nested objects', () => {
|
|
266
|
+
const target = { a: { b: { c: 1 } } };
|
|
267
|
+
const source = { a: { b: { d: 2 } } };
|
|
268
|
+
const result = deepMerge(target, source);
|
|
269
|
+
|
|
270
|
+
expect(result).toEqual({ a: { b: { c: 1, d: 2 } } });
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('isObject', () => {
|
|
275
|
+
it('returns true for plain objects', () => {
|
|
276
|
+
expect(isObject({})).toBe(true);
|
|
277
|
+
expect(isObject({ a: 1 })).toBe(true);
|
|
278
|
+
expect(isObject({ nested: { value: 1 } })).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns false for arrays', () => {
|
|
282
|
+
expect(isObject([])).toBe(false);
|
|
283
|
+
expect(isObject([1, 2, 3])).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns false for null', () => {
|
|
287
|
+
expect(isObject(null)).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('returns false for primitives', () => {
|
|
291
|
+
expect(isObject(undefined)).toBe(false);
|
|
292
|
+
expect(isObject('string')).toBe(false);
|
|
293
|
+
expect(isObject(123)).toBe(false);
|
|
294
|
+
expect(isObject(true)).toBe(false);
|
|
295
|
+
expect(isObject(Symbol('test'))).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('returns false for functions', () => {
|
|
299
|
+
expect(isObject(() => {})).toBe(false);
|
|
300
|
+
expect(isObject(function() {})).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('returns true for Date objects', () => {
|
|
304
|
+
// Date is technically an object type
|
|
305
|
+
expect(isObject(new Date())).toBe(true);
|
|
306
|
+
});
|
|
83
307
|
});
|
|
84
308
|
});
|