@jmruthers/pace-core 0.6.7 → 0.6.8
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/audit-tool/00-dependencies.cjs +215 -9
- package/audit-tool/audits/02-project-structure.cjs +3 -18
- package/audit-tool/audits/03-architecture.cjs +34 -6
- package/audit-tool/audits/06-security-rbac.cjs +10 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +55 -1
- package/audit-tool/index.cjs +23 -19
- package/audit-tool/utils/report-utils.cjs +141 -2
- package/dist/{DataTable-7PMH7XN7.js → DataTable-6RMSCQJ6.js} +5 -5
- package/dist/{PublicPageProvider-DlsCaR5v.d.ts → PublicPageProvider-CIGSujI2.d.ts} +14 -8
- package/dist/{UnifiedAuthProvider-ZT6TIGM7.js → UnifiedAuthProvider-7SNDOWYD.js} +2 -2
- package/dist/{api-Y4MQWOFW.js → api-7P7DI652.js} +1 -1
- package/dist/{chunk-L4XMVJKY.js → chunk-4DDCYDQ3.js} +8 -7
- package/dist/{chunk-ZKAWKYT4.js → chunk-5W2A3DRC.js} +2 -1
- package/dist/{chunk-VBCS3DUA.js → chunk-EF2UGZWY.js} +3 -3
- package/dist/{chunk-JGWDVX64.js → chunk-EURB7QFZ.js} +123 -53
- package/dist/{chunk-BM4CQ5P3.js → chunk-GS5672WG.js} +6 -6
- package/dist/{chunk-ZFYPMX46.js → chunk-LX6U42O3.js} +1 -1
- package/dist/{chunk-5X4QLXRG.js → chunk-MPBLMWVR.js} +5 -3
- package/dist/{chunk-Q7Q7V5NV.js → chunk-NKHKXPI4.js} +7 -7
- package/dist/{chunk-6F3IILHI.js → chunk-S6ZQKDY6.js} +1 -1
- package/dist/{chunk-FTCRZOG2.js → chunk-T5CVK4R3.js} +5 -5
- package/dist/{chunk-GHYHJTYV.js → chunk-Z2FNRKF3.js} +13 -13
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -12
- package/dist/eslint-rules/rules/04-code-quality.cjs +66 -10
- package/dist/eslint-rules/rules/06-security-rbac.cjs +8 -3
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +190 -68
- package/dist/{functions-DHebl8-F.d.ts → functions-lBy5L2ry.d.ts} +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -15
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +6 -6
- package/dist/theming/runtime.d.ts +48 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/modules.md +63 -14
- package/docs/getting-started/dependencies.md +23 -0
- package/docs/implementation-guides/app-layout.md +1 -1
- package/docs/implementation-guides/data-tables.md +1 -1
- package/docs/standards/1-pace-core-compliance-standards.md +38 -1
- package/eslint-config-pace-core.cjs +30 -11
- package/package.json +45 -15
- package/scripts/eslint-audit.cjs +123 -0
- package/scripts/install-eslint-config.cjs +67 -2
- package/scripts/validate-dependencies.cjs +248 -0
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +20 -8
- package/src/__tests__/templates/accessibility.test.template.tsx +1 -0
- package/src/components/AddressField/AddressField.tsx +26 -1
- package/src/components/Alert/Alert.test.tsx +86 -22
- package/src/components/Alert/Alert.tsx +19 -11
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Checkbox/Checkbox.test.tsx +2 -1
- package/src/components/ContextSelector/ContextSelector.tsx +39 -41
- package/src/components/DataTable/DataTable.tsx +1 -19
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +6 -10
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +18 -9
- package/src/components/DataTable/__tests__/pagination.modes.test.tsx +3 -2
- package/src/components/DataTable/components/EmptyState.tsx +1 -1
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +1 -1
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +3 -3
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +33 -29
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +1 -2
- package/src/components/FileUpload/FileUpload.test.tsx +22 -31
- package/src/components/FileUpload/FileUpload.tsx +29 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +48 -12
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +30 -30
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +4 -4
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +7 -1
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +8 -5
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +4 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +3 -3
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +45 -8
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +22 -2
- package/src/hooks/public/usePublicRouteParams.ts +8 -4
- package/src/hooks/useAddressAutocomplete.test.ts +18 -18
- package/src/hooks/useEventTheme.ts +5 -1
- package/src/hooks/useFileUrl.ts +52 -8
- package/src/hooks/useOrganisationSecurity.test.ts +2 -1
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +1 -1
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +15 -6
- package/src/rbac/__tests__/rbac-functions.test.ts +3 -3
- package/src/rbac/api.test.ts +104 -0
- package/src/rbac/engine.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +2 -2
- package/src/rbac/secureClient.ts +1 -1
- package/src/rbac/types/functions.ts +1 -1
- package/src/theming/__tests__/parseEventColours.test.ts +117 -8
- package/src/theming/parseEventColours.ts +56 -2
- package/src/types/supabase.ts +2 -3
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +9 -9
- package/src/utils/file-reference/__tests__/file-reference.test.ts +4 -0
- package/src/utils/formatting/formatDate.test.ts +3 -2
- package/src/utils/formatting/formatDateTime.test.ts +2 -2
- package/src/utils/google-places/googlePlacesUtils.test.ts +36 -24
- package/src/utils/storage/__tests__/helpers.unit.test.ts +19 -12
- package/src/utils/storage/helpers.test.ts +69 -3
|
@@ -444,7 +444,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
444
444
|
});
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
-
it('prevents access when user lacks permission', async () => {
|
|
447
|
+
it('prevents access when user lacks permission', { timeout: 3000 }, async () => {
|
|
448
448
|
// Ensure super admin check completes first
|
|
449
449
|
mockIsSuperAdmin.mockResolvedValueOnce(false);
|
|
450
450
|
|
|
@@ -465,7 +465,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
465
465
|
// Wait for super admin check to complete and component to re-render
|
|
466
466
|
await waitFor(() => {
|
|
467
467
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
468
|
-
}
|
|
468
|
+
});
|
|
469
469
|
|
|
470
470
|
// Wait a bit for the component to process the super admin check result
|
|
471
471
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -474,8 +474,8 @@ describe('PaceAppLayout Security', () => {
|
|
|
474
474
|
await waitFor(() => {
|
|
475
475
|
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
476
476
|
expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument();
|
|
477
|
-
}
|
|
478
|
-
}
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
479
|
|
|
480
480
|
it('enforces route-specific permissions', async () => {
|
|
481
481
|
const routePermissions: Record<string, Operation> = {
|
|
@@ -501,7 +501,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
501
501
|
});
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
-
it('handles permission check failures securely', async () => {
|
|
504
|
+
it('handles permission check failures securely', { timeout: 3000 }, async () => {
|
|
505
505
|
// Ensure super admin check completes first
|
|
506
506
|
mockIsSuperAdmin.mockResolvedValueOnce(false);
|
|
507
507
|
|
|
@@ -522,7 +522,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
522
522
|
// Wait for super admin check to complete and component to re-render
|
|
523
523
|
await waitFor(() => {
|
|
524
524
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
525
|
-
}
|
|
525
|
+
});
|
|
526
526
|
|
|
527
527
|
// Wait a bit for the component to process the super admin check result
|
|
528
528
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -531,8 +531,8 @@ describe('PaceAppLayout Security', () => {
|
|
|
531
531
|
// When permission check throws an error, should show Permission Error page
|
|
532
532
|
expect(screen.getByText('Permission Error')).toBeInTheDocument();
|
|
533
533
|
expect(screen.getByText('Permission check failed')).toBeInTheDocument();
|
|
534
|
-
}
|
|
535
|
-
}
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
536
|
|
|
537
537
|
it('prevents bypassing permission checks', async () => {
|
|
538
538
|
// Test that permission checks cannot be bypassed by manipulating props
|
|
@@ -562,7 +562,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
562
562
|
});
|
|
563
563
|
|
|
564
564
|
describe('Navigation Security', () => {
|
|
565
|
-
it('filters navigation items based on permissions', async () => {
|
|
565
|
+
it('filters navigation items based on permissions', { timeout: 3000 }, async () => {
|
|
566
566
|
// Mock permission check to allow all permissions for navigation filtering
|
|
567
567
|
// The navigation filtering uses getPermissionMap which is already mocked
|
|
568
568
|
render(
|
|
@@ -582,8 +582,8 @@ describe('PaceAppLayout Security', () => {
|
|
|
582
582
|
// With permission enforcement enabled, the component should render normally
|
|
583
583
|
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
584
584
|
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
585
|
-
}
|
|
586
|
-
}
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
587
|
|
|
588
588
|
it('prevents navigation to unauthorized routes', () => {
|
|
589
589
|
render(
|
|
@@ -843,7 +843,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
843
843
|
});
|
|
844
844
|
});
|
|
845
845
|
|
|
846
|
-
it('handles permission errors securely', async () => {
|
|
846
|
+
it('handles permission errors securely', { timeout: 3000 }, async () => {
|
|
847
847
|
// Ensure super admin check completes first
|
|
848
848
|
mockIsSuperAdmin.mockResolvedValueOnce(false);
|
|
849
849
|
|
|
@@ -864,17 +864,17 @@ describe('PaceAppLayout Security', () => {
|
|
|
864
864
|
// Wait for super admin check to complete and component to re-render
|
|
865
865
|
await waitFor(() => {
|
|
866
866
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
867
|
-
}
|
|
867
|
+
});
|
|
868
868
|
|
|
869
869
|
// Wait a bit for the component to process the super admin check result
|
|
870
870
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
871
871
|
|
|
872
872
|
await waitFor(() => {
|
|
873
873
|
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
874
|
-
}
|
|
875
|
-
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
876
|
|
|
877
|
-
it('prevents information leakage in error messages', async () => {
|
|
877
|
+
it('prevents information leakage in error messages', { timeout: 3000 }, async () => {
|
|
878
878
|
// Ensure super admin check completes first
|
|
879
879
|
mockIsSuperAdmin.mockResolvedValueOnce(false);
|
|
880
880
|
|
|
@@ -902,7 +902,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
902
902
|
// Wait for super admin check to complete and component to re-render
|
|
903
903
|
await waitFor(() => {
|
|
904
904
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
905
|
-
}
|
|
905
|
+
});
|
|
906
906
|
|
|
907
907
|
// Wait a bit for the component to process the super admin check result
|
|
908
908
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -911,8 +911,8 @@ describe('PaceAppLayout Security', () => {
|
|
|
911
911
|
expect(screen.getByText('Permission Error')).toBeInTheDocument();
|
|
912
912
|
// Should not expose sensitive information
|
|
913
913
|
expect(screen.queryByText('password=secret123')).not.toBeInTheDocument();
|
|
914
|
-
}
|
|
915
|
-
}
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
916
|
});
|
|
917
917
|
|
|
918
918
|
describe('Session Security', () => {
|
|
@@ -987,7 +987,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
987
987
|
});
|
|
988
988
|
});
|
|
989
989
|
|
|
990
|
-
it('allows super admin to bypass all permission checks', async () => {
|
|
990
|
+
it('allows super admin to bypass all permission checks', { timeout: 3000 }, async () => {
|
|
991
991
|
// Mock super admin status
|
|
992
992
|
mockIsSuperAdmin.mockResolvedValueOnce(true);
|
|
993
993
|
|
|
@@ -1017,10 +1017,10 @@ describe('PaceAppLayout Security', () => {
|
|
|
1017
1017
|
expect(screen.getByTestId('mock-outlet')).toBeInTheDocument();
|
|
1018
1018
|
// Should NOT show access denied
|
|
1019
1019
|
expect(screen.queryByText('Access Denied')).not.toBeInTheDocument();
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
1022
|
|
|
1023
|
-
it('does not log strict mode violations for super admins', async () => {
|
|
1023
|
+
it('does not log strict mode violations for super admins', { timeout: 5000 }, async () => {
|
|
1024
1024
|
// Mock super admin status (resolve immediately)
|
|
1025
1025
|
mockIsSuperAdmin.mockResolvedValueOnce(true);
|
|
1026
1026
|
|
|
@@ -1052,12 +1052,12 @@ describe('PaceAppLayout Security', () => {
|
|
|
1052
1052
|
// Wait for super admin check to complete
|
|
1053
1053
|
await waitFor(() => {
|
|
1054
1054
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
1055
|
-
}
|
|
1055
|
+
});
|
|
1056
1056
|
|
|
1057
1057
|
// Wait for component to fully render (super admin should bypass checks)
|
|
1058
1058
|
await waitFor(() => {
|
|
1059
1059
|
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
1060
|
-
}
|
|
1060
|
+
});
|
|
1061
1061
|
|
|
1062
1062
|
// Clear any violations that might have been logged before super admin check completed
|
|
1063
1063
|
consoleSpy.mockClear();
|
|
@@ -1073,9 +1073,9 @@ describe('PaceAppLayout Security', () => {
|
|
|
1073
1073
|
expect(violationLogs).toHaveLength(0);
|
|
1074
1074
|
|
|
1075
1075
|
consoleSpy.mockRestore();
|
|
1076
|
-
}
|
|
1076
|
+
});
|
|
1077
1077
|
|
|
1078
|
-
it('prevents privilege escalation', async () => {
|
|
1078
|
+
it('prevents privilege escalation', { timeout: 3000 }, async () => {
|
|
1079
1079
|
// Test that users cannot escalate their privileges
|
|
1080
1080
|
// Create a test wrapper with admin path for privilege escalation test
|
|
1081
1081
|
const AdminTestWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
@@ -1115,7 +1115,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
1115
1115
|
// Wait for super admin check to complete and component to re-render
|
|
1116
1116
|
await waitFor(() => {
|
|
1117
1117
|
expect(mockIsSuperAdmin).toHaveBeenCalled();
|
|
1118
|
-
}
|
|
1118
|
+
});
|
|
1119
1119
|
|
|
1120
1120
|
// Wait a bit for the component to process the super admin check result
|
|
1121
1121
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -1124,7 +1124,7 @@ describe('PaceAppLayout Security', () => {
|
|
|
1124
1124
|
// With privilege escalation prevention, should show access denied for admin
|
|
1125
1125
|
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
1126
1126
|
expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument();
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
1129
|
});
|
|
1130
1130
|
});
|
|
@@ -873,7 +873,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
873
873
|
});
|
|
874
874
|
|
|
875
875
|
describe('Route-Specific Permissions', () => {
|
|
876
|
-
it('uses route-specific permissions when provided', async () => {
|
|
876
|
+
it('uses route-specific permissions when provided', { timeout: 3000 }, async () => {
|
|
877
877
|
renderWithProviders(
|
|
878
878
|
<TestWrapper>
|
|
879
879
|
<PaceAppLayout
|
|
@@ -906,9 +906,9 @@ describe('PaceAppLayout Component', () => {
|
|
|
906
906
|
'Test App'
|
|
907
907
|
);
|
|
908
908
|
}, { timeout: 2000 });
|
|
909
|
-
}
|
|
909
|
+
});
|
|
910
910
|
|
|
911
|
-
it('uses default permission when route not in routePermissions', async () => {
|
|
911
|
+
it('uses default permission when route not in routePermissions', { timeout: 3000 }, async () => {
|
|
912
912
|
renderWithProviders(
|
|
913
913
|
<TestWrapper>
|
|
914
914
|
<PaceAppLayout
|
|
@@ -940,7 +940,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
940
940
|
'Test App'
|
|
941
941
|
);
|
|
942
942
|
}, { timeout: 2000 });
|
|
943
|
-
}
|
|
943
|
+
});
|
|
944
944
|
});
|
|
945
945
|
|
|
946
946
|
describe('Super Admin Bypass', () => {
|
|
@@ -673,7 +673,9 @@ describe('PaceLoginPage Component', () => {
|
|
|
673
673
|
});
|
|
674
674
|
|
|
675
675
|
it('shows access error when required app configuration is missing', async () => {
|
|
676
|
+
vi.mocked(isSuperAdmin).mockResolvedValue(false);
|
|
676
677
|
mockAuthContext.isAuthenticated = true;
|
|
678
|
+
mockAuthContext.isLoading = false;
|
|
677
679
|
mockAuthContext.user = { id: 'user-1' } as User;
|
|
678
680
|
const missingAppSupabase = {
|
|
679
681
|
from: vi.fn((table: string) => {
|
|
@@ -707,12 +709,15 @@ describe('PaceLoginPage Component', () => {
|
|
|
707
709
|
);
|
|
708
710
|
|
|
709
711
|
await waitFor(() => {
|
|
712
|
+
// Error message format: Application "${appName}" is not configured. Please contact your administrator.
|
|
710
713
|
expect(screen.getByText(/is not configured/i)).toBeInTheDocument();
|
|
711
|
-
});
|
|
714
|
+
}, { timeout: 5000 });
|
|
712
715
|
});
|
|
713
716
|
|
|
714
717
|
it('shows organisation access error when user lacks active organisations', async () => {
|
|
718
|
+
vi.mocked(isSuperAdmin).mockResolvedValue(false);
|
|
715
719
|
mockAuthContext.isAuthenticated = true;
|
|
720
|
+
mockAuthContext.isLoading = false;
|
|
716
721
|
mockAuthContext.user = { id: 'user-2' } as User;
|
|
717
722
|
|
|
718
723
|
const createOrganisationRolesQuery = (result: { data: unknown; error: unknown }) => {
|
|
@@ -779,6 +784,7 @@ describe('PaceLoginPage Component', () => {
|
|
|
779
784
|
);
|
|
780
785
|
|
|
781
786
|
await waitFor(() => {
|
|
787
|
+
// Error message format: You do not have permission to access ${appName}. You are not assigned to any organisation. Please contact your administrator.
|
|
782
788
|
expect(screen.getByText(/not assigned to any organisation/i)).toBeInTheDocument();
|
|
783
789
|
}, { timeout: 5000 });
|
|
784
790
|
});
|
|
@@ -67,11 +67,14 @@ vi.mock('../../components/DataTable/utils/errorHandling', () => ({
|
|
|
67
67
|
// Constructor
|
|
68
68
|
}
|
|
69
69
|
} as any,
|
|
70
|
-
CircuitBreaker: vi.fn(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
CircuitBreaker: vi.fn(function CircuitBreaker() {
|
|
71
|
+
return {
|
|
72
|
+
isOpen: vi.fn(() => false),
|
|
73
|
+
recordSuccess: vi.fn(),
|
|
74
|
+
recordFailure: vi.fn(),
|
|
75
|
+
reset: vi.fn()
|
|
76
|
+
};
|
|
77
|
+
}) as any,
|
|
75
78
|
DEFAULT_FALLBACK_CONFIG: {
|
|
76
79
|
enableFallbacks: true,
|
|
77
80
|
maxRetries: 3
|
|
@@ -820,12 +820,16 @@ describe('useFileUrl Hook', () => {
|
|
|
820
820
|
}
|
|
821
821
|
);
|
|
822
822
|
|
|
823
|
+
// Wait for initial URL to load
|
|
823
824
|
await waitFor(
|
|
824
825
|
() => {
|
|
825
826
|
expect(result.current.url).not.toBe(null);
|
|
826
827
|
},
|
|
827
828
|
{ timeout: 2000 }
|
|
828
829
|
);
|
|
830
|
+
|
|
831
|
+
// Verify initial URL is set
|
|
832
|
+
expect(result.current.url).toBe('https://example.com/org-123/logos/logo.png');
|
|
829
833
|
|
|
830
834
|
const newFile = { ...mockPublicFileReference, id: 'file-999', file_path: 'org-123/logos/new-logo.png' };
|
|
831
835
|
rerender({ fileRef: newFile });
|
|
@@ -634,7 +634,7 @@ describe('useFocusTrap', () => {
|
|
|
634
634
|
getByTestId('toggle').click();
|
|
635
635
|
});
|
|
636
636
|
|
|
637
|
-
expect(screen.queryByTestId('container')).
|
|
637
|
+
expect(screen.queryByTestId('container')).toBeNull();
|
|
638
638
|
});
|
|
639
639
|
});
|
|
640
640
|
|
|
@@ -711,13 +711,13 @@ describe('useFocusTrap', () => {
|
|
|
711
711
|
|
|
712
712
|
// Wait for React to update the DOM after state change
|
|
713
713
|
await waitFor(() => {
|
|
714
|
-
expect(screen.queryByTestId('button-Button 2')).
|
|
714
|
+
expect(screen.queryByTestId('button-Button 2')).toBeNull();
|
|
715
715
|
});
|
|
716
716
|
|
|
717
717
|
// Verify the removed button is gone and others remain
|
|
718
718
|
expect(screen.getByTestId('button-Button 1')).toBeDefined();
|
|
719
719
|
expect(screen.getByTestId('button-Button 3')).toBeDefined();
|
|
720
|
-
|
|
720
|
+
expect(screen.queryByTestId('button-Button 2')).toBeNull();
|
|
721
721
|
});
|
|
722
722
|
|
|
723
723
|
it('should handle elements becoming disabled/enabled', () => {
|
|
@@ -288,15 +288,29 @@ describe('useInactivityTracker', () => {
|
|
|
288
288
|
});
|
|
289
289
|
|
|
290
290
|
it('broadcasts activity to other tabs', () => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
// Ensure no persisted time to force resetActivity call
|
|
292
|
+
localStorageMock.getItem.mockReturnValue(null);
|
|
293
|
+
|
|
294
|
+
// Start with enabled: true to allow tracking
|
|
295
|
+
const { result } = renderHook(
|
|
296
|
+
({ enabled }) => useInactivityTracker({
|
|
297
|
+
enabled,
|
|
294
298
|
channelName: 'test-channel'
|
|
295
|
-
})
|
|
299
|
+
}),
|
|
300
|
+
{ initialProps: { enabled: true } }
|
|
296
301
|
);
|
|
297
302
|
|
|
303
|
+
// Wait for automatic initialization
|
|
298
304
|
act(() => {
|
|
299
|
-
|
|
305
|
+
vi.advanceTimersByTime(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Clear any existing calls from initialization
|
|
309
|
+
mockBroadcastChannel.postMessage.mockClear();
|
|
310
|
+
|
|
311
|
+
// Trigger activity reset which should broadcast
|
|
312
|
+
act(() => {
|
|
313
|
+
result.current.resetActivity();
|
|
300
314
|
});
|
|
301
315
|
|
|
302
316
|
expect(mockBroadcastChannel.postMessage).toHaveBeenCalledWith({
|
|
@@ -337,16 +351,39 @@ describe('useInactivityTracker', () => {
|
|
|
337
351
|
|
|
338
352
|
it('cleans up timers and listeners on unmount', () => {
|
|
339
353
|
const { result, unmount } = renderHook(() =>
|
|
340
|
-
useInactivityTracker({ enabled: true })
|
|
354
|
+
useInactivityTracker({ enabled: true, channelName: 'test-channel' })
|
|
341
355
|
);
|
|
342
356
|
|
|
357
|
+
// Wait for automatic initialization
|
|
343
358
|
act(() => {
|
|
344
|
-
|
|
359
|
+
vi.advanceTimersByTime(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Ensure tracking is active and channel/listeners are set up
|
|
363
|
+
expect(result.current.isTracking).toBe(true);
|
|
364
|
+
expect(mockAddEventListener).toHaveBeenCalled();
|
|
365
|
+
|
|
366
|
+
// Verify channel was created
|
|
367
|
+
expect(window.BroadcastChannel).toHaveBeenCalled();
|
|
368
|
+
|
|
369
|
+
// Clear mocks to track cleanup calls
|
|
370
|
+
mockRemoveEventListener.mockClear();
|
|
371
|
+
mockBroadcastChannel.close.mockClear();
|
|
372
|
+
|
|
373
|
+
// Unmount should trigger cleanup
|
|
374
|
+
act(() => {
|
|
375
|
+
unmount();
|
|
345
376
|
});
|
|
346
377
|
|
|
347
|
-
|
|
378
|
+
// Advance timers to ensure cleanup completes
|
|
379
|
+
act(() => {
|
|
380
|
+
vi.advanceTimersByTime(100);
|
|
381
|
+
});
|
|
348
382
|
|
|
383
|
+
// Verify cleanup was called
|
|
384
|
+
// The hook should remove all event listeners that were added
|
|
349
385
|
expect(mockRemoveEventListener).toHaveBeenCalled();
|
|
386
|
+
// Channel cleanup happens in the unmount effect
|
|
350
387
|
expect(mockBroadcastChannel.close).toHaveBeenCalled();
|
|
351
388
|
});
|
|
352
389
|
|
|
@@ -610,7 +610,13 @@ describe('useOperationPerformance', () => {
|
|
|
610
610
|
useOperationPerformance('testOperation', 'OPERATION')
|
|
611
611
|
);
|
|
612
612
|
|
|
613
|
-
|
|
613
|
+
// Clear any previous calls
|
|
614
|
+
vi.clearAllMocks();
|
|
615
|
+
|
|
616
|
+
// Use mockReturnValueOnce to ensure exact sequence
|
|
617
|
+
mockPerformanceNow
|
|
618
|
+
.mockReturnValueOnce(0) // Start time
|
|
619
|
+
.mockReturnValueOnce(20); // End time
|
|
614
620
|
|
|
615
621
|
const operation = () => 'result';
|
|
616
622
|
|
|
@@ -623,12 +629,22 @@ describe('useOperationPerformance', () => {
|
|
|
623
629
|
operation: 'testOperation',
|
|
624
630
|
})
|
|
625
631
|
);
|
|
632
|
+
|
|
633
|
+
// Reset mock
|
|
634
|
+
mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
|
|
626
635
|
});
|
|
627
636
|
|
|
628
637
|
it('includes context in measurement', async () => {
|
|
629
638
|
const { result } = renderHook(() => useOperationPerformance('testOperation'));
|
|
630
639
|
|
|
631
|
-
|
|
640
|
+
// Use a sequence to ensure correct values are returned
|
|
641
|
+
let callCount = 0;
|
|
642
|
+
mockPerformanceNow.mockImplementation(() => {
|
|
643
|
+
callCount++;
|
|
644
|
+
if (callCount === 1) return 0; // Start time
|
|
645
|
+
if (callCount === 2) return 15; // End time
|
|
646
|
+
return mockPerformanceNowValue; // Fallback
|
|
647
|
+
});
|
|
632
648
|
|
|
633
649
|
const operation = () => 'result';
|
|
634
650
|
const context = { userId: '123', action: 'test' };
|
|
@@ -644,6 +660,10 @@ describe('useOperationPerformance', () => {
|
|
|
644
660
|
action: 'test',
|
|
645
661
|
})
|
|
646
662
|
);
|
|
663
|
+
|
|
664
|
+
// Reset mock
|
|
665
|
+
callCount = 0;
|
|
666
|
+
mockPerformanceNow.mockImplementation(() => mockPerformanceNowValue);
|
|
647
667
|
});
|
|
648
668
|
|
|
649
669
|
it('handles operation errors', async () => {
|
|
@@ -182,16 +182,20 @@ export function usePublicRouteParams(
|
|
|
182
182
|
|
|
183
183
|
// Determine the final error state
|
|
184
184
|
const finalError = useMemo(() => {
|
|
185
|
+
// Always show validation errors (format validation)
|
|
185
186
|
if (error) return error;
|
|
186
|
-
|
|
187
|
+
// When validation is disabled, ignore event errors (invalid codes are allowed)
|
|
188
|
+
if (!validateEventCode) return null;
|
|
189
|
+
// Include eventError if we're fetching event data and validation is enabled
|
|
190
|
+
if (fetchEventData && eventError) return eventError;
|
|
187
191
|
return null;
|
|
188
|
-
}, [error, eventError]);
|
|
192
|
+
}, [error, eventError, fetchEventData, validateEventCode]);
|
|
189
193
|
|
|
190
194
|
// Extract event ID from event data
|
|
191
195
|
const eventId = useMemo(() => {
|
|
192
|
-
if (!event) return null;
|
|
196
|
+
if (!fetchEventData || !event) return null;
|
|
193
197
|
return event.event_id || event.id;
|
|
194
|
-
}, [event]);
|
|
198
|
+
}, [fetchEventData, event]);
|
|
195
199
|
|
|
196
200
|
// Refetch function
|
|
197
201
|
const refetch = useCallback(async (): Promise<void> => {
|
|
@@ -45,17 +45,17 @@ describe('useAddressAutocomplete', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe('Initial state', () => {
|
|
48
|
-
it('returns initial state with empty suggestions', () => {
|
|
48
|
+
it('returns initial state with empty suggestions', { timeout: 5000 }, () => {
|
|
49
49
|
const { result } = renderHook(() => useAddressAutocomplete(mockApiKey, ''));
|
|
50
50
|
|
|
51
51
|
expect(result.current.suggestions).toEqual([]);
|
|
52
52
|
expect(result.current.isLoading).toBe(false);
|
|
53
53
|
expect(result.current.error).toBeNull();
|
|
54
|
-
}
|
|
54
|
+
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
describe('Autocomplete suggestions', () => {
|
|
58
|
-
it('fetches suggestions when input value changes', async () => {
|
|
58
|
+
it('fetches suggestions when input value changes', { timeout: 5000 }, async () => {
|
|
59
59
|
const mockPredictions = [
|
|
60
60
|
{
|
|
61
61
|
description: '123 Main St, Melbourne VIC, Australia',
|
|
@@ -81,9 +81,9 @@ describe('useAddressAutocomplete', () => {
|
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
expect(result.current.suggestions[0].place_id).toBe('ChIJ123');
|
|
84
|
-
}
|
|
84
|
+
});
|
|
85
85
|
|
|
86
|
-
it('debounces input value', async () => {
|
|
86
|
+
it('debounces input value', { timeout: 5000 }, async () => {
|
|
87
87
|
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValue([]);
|
|
88
88
|
|
|
89
89
|
// Since we're mocking useDebounce to return immediately, this test verifies
|
|
@@ -108,9 +108,9 @@ describe('useAddressAutocomplete', () => {
|
|
|
108
108
|
|
|
109
109
|
// With mocked debounce, it will be called for each change
|
|
110
110
|
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
111
|
-
}
|
|
111
|
+
});
|
|
112
112
|
|
|
113
|
-
it('clears suggestions when input is empty', async () => {
|
|
113
|
+
it('clears suggestions when input is empty', { timeout: 5000 }, async () => {
|
|
114
114
|
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
|
|
115
115
|
{ description: '123 Main St', place_id: 'ChIJ123' },
|
|
116
116
|
]);
|
|
@@ -137,9 +137,9 @@ describe('useAddressAutocomplete', () => {
|
|
|
137
137
|
},
|
|
138
138
|
{ timeout: 1000 }
|
|
139
139
|
);
|
|
140
|
-
}
|
|
140
|
+
});
|
|
141
141
|
|
|
142
|
-
it('handles API errors', async () => {
|
|
142
|
+
it('handles API errors', { timeout: 5000 }, async () => {
|
|
143
143
|
const error = new Error('API request denied');
|
|
144
144
|
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockRejectedValueOnce(error);
|
|
145
145
|
|
|
@@ -156,11 +156,11 @@ describe('useAddressAutocomplete', () => {
|
|
|
156
156
|
|
|
157
157
|
expect(result.current.error?.message).toBe('API request denied');
|
|
158
158
|
expect(result.current.suggestions).toEqual([]);
|
|
159
|
-
}
|
|
159
|
+
});
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
describe('Address selection', () => {
|
|
163
|
-
it('selects address by place_id', async () => {
|
|
163
|
+
it('selects address by place_id', { timeout: 5000 }, async () => {
|
|
164
164
|
const mockPlaceDetails = {
|
|
165
165
|
place_id: 'ChIJ123',
|
|
166
166
|
formatted_address: '123 Main St, Melbourne VIC 3000, Australia',
|
|
@@ -191,7 +191,7 @@ describe('useAddressAutocomplete', () => {
|
|
|
191
191
|
expect(address).not.toBeNull();
|
|
192
192
|
expect(address?.place_id).toBe('ChIJ123');
|
|
193
193
|
expect(googlePlacesUtils.fetchPlaceDetails).toHaveBeenCalledWith('ChIJ123', mockApiKey);
|
|
194
|
-
}
|
|
194
|
+
});
|
|
195
195
|
|
|
196
196
|
it('returns null when place_id is invalid', async () => {
|
|
197
197
|
vi.mocked(googlePlacesUtils.fetchPlaceDetails).mockRejectedValueOnce(new Error('Place not found'));
|
|
@@ -205,7 +205,7 @@ describe('useAddressAutocomplete', () => {
|
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
describe('getAddressByPlaceId', () => {
|
|
208
|
-
it('retrieves address by place_id', async () => {
|
|
208
|
+
it('retrieves address by place_id', { timeout: 5000 }, async () => {
|
|
209
209
|
const mockAddress = {
|
|
210
210
|
place_id: 'ChIJ123',
|
|
211
211
|
full_address: '123 Main St',
|
|
@@ -227,7 +227,7 @@ describe('useAddressAutocomplete', () => {
|
|
|
227
227
|
|
|
228
228
|
expect(address).not.toBeNull();
|
|
229
229
|
expect(address?.place_id).toBe('ChIJ123');
|
|
230
|
-
}
|
|
230
|
+
});
|
|
231
231
|
|
|
232
232
|
it('returns null on error', async () => {
|
|
233
233
|
vi.mocked(googlePlacesUtils.getAddressByPlaceId).mockResolvedValueOnce(null);
|
|
@@ -237,11 +237,11 @@ describe('useAddressAutocomplete', () => {
|
|
|
237
237
|
const address = await result.current.getAddressByPlaceId('invalid');
|
|
238
238
|
|
|
239
239
|
expect(address).toBeNull();
|
|
240
|
-
}
|
|
240
|
+
});
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
describe('clearSuggestions', () => {
|
|
244
|
-
it('clears suggestions', async () => {
|
|
244
|
+
it('clears suggestions', { timeout: 5000 }, async () => {
|
|
245
245
|
vi.mocked(googlePlacesUtils.fetchPlaceAutocomplete).mockResolvedValueOnce([
|
|
246
246
|
{ description: '123 Main St', place_id: 'ChIJ123' },
|
|
247
247
|
]);
|
|
@@ -268,7 +268,7 @@ describe('useAddressAutocomplete', () => {
|
|
|
268
268
|
},
|
|
269
269
|
{ timeout: 1000 }
|
|
270
270
|
);
|
|
271
|
-
}
|
|
271
|
+
});
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
describe('Caching', () => {
|
|
@@ -312,7 +312,7 @@ describe('useAddressAutocomplete', () => {
|
|
|
312
312
|
);
|
|
313
313
|
|
|
314
314
|
expect(googlePlacesUtils.fetchPlaceAutocomplete).toHaveBeenCalled();
|
|
315
|
-
}
|
|
315
|
+
});
|
|
316
316
|
});
|
|
317
317
|
});
|
|
318
318
|
|
|
@@ -43,8 +43,11 @@ import { useLocation } from 'react-router-dom';
|
|
|
43
43
|
import { EventServiceContext } from '../providers/services/EventServiceProvider';
|
|
44
44
|
import { applyPalette, clearPalette } from '../theming/runtime';
|
|
45
45
|
import { parseAndNormalizeEventColours } from '../theming/parseEventColours';
|
|
46
|
+
import { createLogger } from '../utils/core/logger';
|
|
46
47
|
import type { Event } from '../types/event';
|
|
47
48
|
|
|
49
|
+
const log = createLogger('useEventTheme');
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* Hook that automatically applies event-specific theming
|
|
50
53
|
*
|
|
@@ -144,7 +147,8 @@ export function useEventTheme(event?: Event | null): void {
|
|
|
144
147
|
try {
|
|
145
148
|
applyPalette(normalized);
|
|
146
149
|
} catch (error) {
|
|
147
|
-
//
|
|
150
|
+
// Log error but don't throw - theming is not critical
|
|
151
|
+
log.error('Failed to apply event palette:', error);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
// Cleanup function to clear palette when component unmounts or event changes
|