@jmruthers/pace-core 0.5.118 → 0.5.119
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-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
- package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
- package/dist/{chunk-7OTQLFVI.js → chunk-B4GZ2BXO.js} +3 -3
- package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
- package/dist/chunk-BHWIUEYH.js.map +1 -0
- package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
- package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
- package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
- package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
- package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
- package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
- package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
- package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
- package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
- package/dist/components.js +8 -8
- package/dist/hooks.js +7 -7
- package/dist/index.js +11 -11
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- 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 +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/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 +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/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 +2 -2
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
- package/src/hooks/__tests__/index.unit.test.ts +223 -0
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
- package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
- package/src/hooks/useSecureDataAccess.test.ts +1 -0
- package/src/rbac/audit-enhanced.ts +339 -0
- package/src/services/EventService.ts +1 -0
- package/src/services/__tests__/AuthService.test.ts +473 -0
- package/src/services/__tests__/EventService.test.ts +390 -0
- package/src/services/__tests__/InactivityService.test.ts +217 -0
- package/src/services/__tests__/OrganisationService.test.ts +371 -0
- package/dist/chunk-KA3PSVNV.js.map +0 -1
- package/src/components/DataTable/utils/debugTools.ts +0 -609
- package/src/rbac/testing/index.tsx +0 -340
- /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
- /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
- /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
- /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
- /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
- /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
- /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
- /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
- /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
- /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
- /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
|
@@ -522,4 +522,394 @@ describe('EventService', () => {
|
|
|
522
522
|
expect(service.getSelectedEvent()?.event_id).toBe('event-1');
|
|
523
523
|
});
|
|
524
524
|
});
|
|
525
|
+
|
|
526
|
+
describe('Dependency Updates', () => {
|
|
527
|
+
it('should update dependencies when organisation changes', async () => {
|
|
528
|
+
const newOrganisation = { id: 'org-2', display_name: 'New Organisation' };
|
|
529
|
+
|
|
530
|
+
await eventService.updateDependencies(
|
|
531
|
+
mockSupabase as any,
|
|
532
|
+
mockUser,
|
|
533
|
+
mockSession,
|
|
534
|
+
'test-app',
|
|
535
|
+
newOrganisation,
|
|
536
|
+
mockSetSelectedEventId
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// Should reset initialization when organisation changes
|
|
540
|
+
expect(eventService.getEvents()).toBeDefined();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should clear events when switching to different organisation', async () => {
|
|
544
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
545
|
+
data: [mockEvent],
|
|
546
|
+
error: null
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await eventService.initialize();
|
|
550
|
+
eventService.setSelectedEvent(mockEvent);
|
|
551
|
+
|
|
552
|
+
const newOrganisation = { id: 'org-2', display_name: 'New Organisation' };
|
|
553
|
+
|
|
554
|
+
await eventService.updateDependencies(
|
|
555
|
+
mockSupabase as any,
|
|
556
|
+
mockUser,
|
|
557
|
+
mockSession,
|
|
558
|
+
'test-app',
|
|
559
|
+
newOrganisation,
|
|
560
|
+
mockSetSelectedEventId
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Events should be cleared when switching organisations
|
|
564
|
+
expect(eventService.getEvents()).toEqual([]);
|
|
565
|
+
expect(eventService.getSelectedEvent()).toBeNull();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should clear event selection when user changes', async () => {
|
|
569
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
570
|
+
data: [mockEvent],
|
|
571
|
+
error: null
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await eventService.initialize();
|
|
575
|
+
eventService.setSelectedEvent(mockEvent);
|
|
576
|
+
|
|
577
|
+
const newUser = { id: 'user-2', email: 'new@example.com' };
|
|
578
|
+
const newSession = { access_token: 'new-token', user: newUser };
|
|
579
|
+
|
|
580
|
+
await eventService.updateDependencies(
|
|
581
|
+
mockSupabase as any,
|
|
582
|
+
newUser,
|
|
583
|
+
newSession,
|
|
584
|
+
'test-app',
|
|
585
|
+
mockOrganisation,
|
|
586
|
+
mockSetSelectedEventId
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Event selection should be cleared for previous user
|
|
590
|
+
expect(mockSecureStorage.removeItem).toHaveBeenCalled();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should clear event selection when user logs out', async () => {
|
|
594
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
595
|
+
data: [mockEvent],
|
|
596
|
+
error: null
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
await eventService.initialize();
|
|
600
|
+
eventService.setSelectedEvent(mockEvent);
|
|
601
|
+
|
|
602
|
+
await eventService.updateDependencies(
|
|
603
|
+
mockSupabase as any,
|
|
604
|
+
null,
|
|
605
|
+
null,
|
|
606
|
+
'test-app',
|
|
607
|
+
mockOrganisation,
|
|
608
|
+
mockSetSelectedEventId
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// Event should be cleared on logout
|
|
612
|
+
expect(eventService.getSelectedEvent()).toBeNull();
|
|
613
|
+
expect(mockSetSelectedEventId).toHaveBeenCalledWith(null);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('Event Persistence', () => {
|
|
618
|
+
it('should restore persisted event', async () => {
|
|
619
|
+
mockSecureStorage.getItem.mockResolvedValue('event-1');
|
|
620
|
+
|
|
621
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
622
|
+
data: [mockEvent, mockEvent2],
|
|
623
|
+
error: null
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const service = new EventService(
|
|
627
|
+
mockSupabase as any,
|
|
628
|
+
mockUser,
|
|
629
|
+
mockSession,
|
|
630
|
+
'test-app',
|
|
631
|
+
mockOrganisation,
|
|
632
|
+
mockSetSelectedEventId
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
await service.initialize();
|
|
636
|
+
|
|
637
|
+
const restored = await service.restorePersistedEvent();
|
|
638
|
+
|
|
639
|
+
expect(restored).toBe(true);
|
|
640
|
+
expect(service.getSelectedEvent()).toEqual(mockEvent);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('should return false when restoring persisted event if events not loaded', async () => {
|
|
644
|
+
const service = new EventService(
|
|
645
|
+
mockSupabase as any,
|
|
646
|
+
mockUser,
|
|
647
|
+
mockSession,
|
|
648
|
+
'test-app',
|
|
649
|
+
mockOrganisation,
|
|
650
|
+
mockSetSelectedEventId
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// Don't initialize, so events are empty
|
|
654
|
+
const restored = await service.restorePersistedEvent();
|
|
655
|
+
|
|
656
|
+
expect(restored).toBe(false);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should clear event selection for specific user', async () => {
|
|
660
|
+
await eventService.clearEventSelectionForUser('user-1');
|
|
661
|
+
|
|
662
|
+
expect(mockSecureStorage.removeItem).toHaveBeenCalledWith(
|
|
663
|
+
expect.stringContaining('pace-core-selected-event-user-1')
|
|
664
|
+
);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should handle clearing event selection for null user', async () => {
|
|
668
|
+
await eventService.clearEventSelectionForUser(null);
|
|
669
|
+
|
|
670
|
+
// Should not throw error
|
|
671
|
+
expect(mockSecureStorage.removeItem).not.toHaveBeenCalled();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe('Event Colours Parsing', () => {
|
|
676
|
+
it('should parse event colours from string JSON', () => {
|
|
677
|
+
const eventWithStringColours: Event = {
|
|
678
|
+
...mockEvent,
|
|
679
|
+
event_colours: JSON.stringify({
|
|
680
|
+
main: { '500': '#FF0000' },
|
|
681
|
+
sec: { '500': '#00FF00' },
|
|
682
|
+
acc: { '500': '#0000FF' }
|
|
683
|
+
})
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// The parseAndNormalizeEventColours is private, but we can test it indirectly
|
|
687
|
+
// by checking if setSelectedEvent handles it correctly
|
|
688
|
+
eventService.setSelectedEvent(eventWithStringColours);
|
|
689
|
+
|
|
690
|
+
expect(eventService.getSelectedEvent()).toEqual(eventWithStringColours);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('should parse event colours with ev-* keys', () => {
|
|
694
|
+
const eventWithEvKeys: Event = {
|
|
695
|
+
...mockEvent,
|
|
696
|
+
event_colours: {
|
|
697
|
+
'ev-main': { '500': '#FF0000' },
|
|
698
|
+
'ev-sec': { '500': '#00FF00' },
|
|
699
|
+
'ev-acc': { '500': '#0000FF' }
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
eventService.setSelectedEvent(eventWithEvKeys);
|
|
704
|
+
|
|
705
|
+
expect(eventService.getSelectedEvent()).toEqual(eventWithEvKeys);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should handle invalid event colours gracefully', () => {
|
|
709
|
+
const eventWithInvalidColours: Event = {
|
|
710
|
+
...mockEvent,
|
|
711
|
+
event_colours: 'invalid-json'
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// Should not throw error
|
|
715
|
+
eventService.setSelectedEvent(eventWithInvalidColours);
|
|
716
|
+
|
|
717
|
+
expect(eventService.getSelectedEvent()).toEqual(eventWithInvalidColours);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it('should handle null event colours', () => {
|
|
721
|
+
const eventWithNullColours: Event = {
|
|
722
|
+
...mockEvent,
|
|
723
|
+
event_colours: null as any
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
eventService.setSelectedEvent(eventWithNullColours);
|
|
727
|
+
|
|
728
|
+
expect(eventService.getSelectedEvent()).toEqual(eventWithNullColours);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
describe('Next Event Selection', () => {
|
|
733
|
+
it('should return null when no events provided', () => {
|
|
734
|
+
const nextEvent = eventService.getNextEventByDate([]);
|
|
735
|
+
|
|
736
|
+
expect(nextEvent).toBeNull();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should return null when all events are in the past', () => {
|
|
740
|
+
const pastEvent: Event = {
|
|
741
|
+
...mockEvent,
|
|
742
|
+
event_date: '2020-01-01T00:00:00Z'
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const nextEvent = eventService.getNextEventByDate([pastEvent]);
|
|
746
|
+
|
|
747
|
+
expect(nextEvent).toBeNull();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should select event on today\'s date', () => {
|
|
751
|
+
const today = new Date();
|
|
752
|
+
const todayEvent: Event = {
|
|
753
|
+
...mockEvent,
|
|
754
|
+
event_date: today.toISOString()
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const nextEvent = eventService.getNextEventByDate([todayEvent]);
|
|
758
|
+
|
|
759
|
+
expect(nextEvent).toEqual(todayEvent);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should select earliest future event when multiple future events exist', () => {
|
|
763
|
+
// Create events with dates far in the future to ensure they're both considered future events
|
|
764
|
+
const today = new Date();
|
|
765
|
+
const futureDate1 = new Date(today.getTime() + 200 * 24 * 60 * 60 * 1000); // 200 days from now
|
|
766
|
+
const futureDate2 = new Date(today.getTime() + 100 * 24 * 60 * 60 * 1000); // 100 days from now (earlier)
|
|
767
|
+
|
|
768
|
+
const futureEvent1: Event = {
|
|
769
|
+
id: 'event-1',
|
|
770
|
+
event_id: 'event-1',
|
|
771
|
+
event_name: 'Later Event',
|
|
772
|
+
event_date: futureDate1.toISOString(),
|
|
773
|
+
event_venue: 'Venue',
|
|
774
|
+
event_participants: 100,
|
|
775
|
+
event_colours: '#FF0000',
|
|
776
|
+
event_logo: '',
|
|
777
|
+
organisation_id: 'org-1',
|
|
778
|
+
is_visible: true,
|
|
779
|
+
name: 'Later Event',
|
|
780
|
+
start_date: futureDate1.toISOString()
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const futureEvent2: Event = {
|
|
784
|
+
id: 'event-2',
|
|
785
|
+
event_id: 'event-2',
|
|
786
|
+
event_name: 'Earlier Event',
|
|
787
|
+
event_date: futureDate2.toISOString(),
|
|
788
|
+
event_venue: 'Venue',
|
|
789
|
+
event_participants: 100,
|
|
790
|
+
event_colours: '#00FF00',
|
|
791
|
+
event_logo: '',
|
|
792
|
+
organisation_id: 'org-1',
|
|
793
|
+
is_visible: true,
|
|
794
|
+
name: 'Earlier Event',
|
|
795
|
+
start_date: futureDate2.toISOString()
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const nextEvent = eventService.getNextEventByDate([futureEvent1, futureEvent2]);
|
|
799
|
+
|
|
800
|
+
// Should select the earlier event (event-2, 100 days from now)
|
|
801
|
+
expect(nextEvent?.event_id).toBe('event-2');
|
|
802
|
+
expect(nextEvent?.event_date).toBe(futureDate2.toISOString());
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('should filter out events without dates', () => {
|
|
806
|
+
const eventWithoutDate: Event = {
|
|
807
|
+
...mockEvent,
|
|
808
|
+
event_date: null as any
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const nextEvent = eventService.getNextEventByDate([eventWithoutDate]);
|
|
812
|
+
|
|
813
|
+
expect(nextEvent).toBeNull();
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
describe('Auto-selection Logic', () => {
|
|
818
|
+
it('should auto-select next event when no persisted event exists', async () => {
|
|
819
|
+
mockSecureStorage.getItem.mockResolvedValue(null);
|
|
820
|
+
|
|
821
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
822
|
+
data: [mockEvent, mockEvent2],
|
|
823
|
+
error: null
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const service = new EventService(
|
|
827
|
+
mockSupabase as any,
|
|
828
|
+
mockUser,
|
|
829
|
+
mockSession,
|
|
830
|
+
'test-app',
|
|
831
|
+
mockOrganisation,
|
|
832
|
+
mockSetSelectedEventId
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
await service.initialize();
|
|
836
|
+
|
|
837
|
+
// Should auto-select the future event
|
|
838
|
+
expect(service.getSelectedEvent()).not.toBeNull();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should not auto-select when user has explicitly cleared event', async () => {
|
|
842
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
843
|
+
data: [mockEvent, mockEvent2],
|
|
844
|
+
error: null
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
await eventService.initialize();
|
|
848
|
+
|
|
849
|
+
// Clear the event (this sets userClearedEventRef)
|
|
850
|
+
eventService.setSelectedEvent(null);
|
|
851
|
+
|
|
852
|
+
// Refresh events
|
|
853
|
+
await eventService.refreshEvents();
|
|
854
|
+
|
|
855
|
+
// Should not auto-select after user cleared
|
|
856
|
+
expect(eventService.getSelectedEvent()).toBeNull();
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe('Storage Cleanup', () => {
|
|
861
|
+
it('should clean up old storage keys on initialization', async () => {
|
|
862
|
+
// Set old storage keys
|
|
863
|
+
sessionStorage.setItem('pace-core-selected-event', 'old-event');
|
|
864
|
+
localStorage.setItem('pace-core-selected-event', 'old-event');
|
|
865
|
+
localStorage.setItem('_sec_pace-core-selected-event', 'old-encrypted');
|
|
866
|
+
|
|
867
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
868
|
+
data: [mockEvent],
|
|
869
|
+
error: null
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
await eventService.initialize();
|
|
873
|
+
|
|
874
|
+
// Old keys should be removed
|
|
875
|
+
expect(sessionStorage.getItem('pace-core-selected-event')).toBeNull();
|
|
876
|
+
expect(localStorage.getItem('pace-core-selected-event')).toBeNull();
|
|
877
|
+
expect(localStorage.getItem('_sec_pace-core-selected-event')).toBeNull();
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
describe('Event Validation', () => {
|
|
882
|
+
it('should handle validation error gracefully', () => {
|
|
883
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
884
|
+
|
|
885
|
+
const eventFromDifferentOrg: Event = {
|
|
886
|
+
...mockEvent,
|
|
887
|
+
organisation_id: 'org-2'
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// Should not throw error
|
|
891
|
+
eventService.setSelectedEvent(eventFromDifferentOrg);
|
|
892
|
+
|
|
893
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
894
|
+
consoleErrorSpy.mockRestore();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('should handle missing organisation during validation', () => {
|
|
898
|
+
const serviceWithoutOrg = new EventService(
|
|
899
|
+
mockSupabase as any,
|
|
900
|
+
mockUser,
|
|
901
|
+
mockSession,
|
|
902
|
+
'test-app',
|
|
903
|
+
null,
|
|
904
|
+
mockSetSelectedEventId
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// Should not throw error
|
|
908
|
+
serviceWithoutOrg.setSelectedEvent(mockEvent);
|
|
909
|
+
|
|
910
|
+
// When organisation is null, validation is skipped (only validates when organisation exists)
|
|
911
|
+
// So the event can still be selected
|
|
912
|
+
expect(serviceWithoutOrg.getSelectedEvent()).toEqual(mockEvent);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
525
915
|
});
|
|
@@ -400,4 +400,221 @@ describe('InactivityService', () => {
|
|
|
400
400
|
expect(inactivityService.getIsTracking()).toBe(false);
|
|
401
401
|
});
|
|
402
402
|
});
|
|
403
|
+
|
|
404
|
+
describe('Dependency Updates', () => {
|
|
405
|
+
it('should update dependencies', () => {
|
|
406
|
+
const newUser = { id: 'user-2', email: 'new@example.com' };
|
|
407
|
+
const newSession = { access_token: 'new-token', user: newUser };
|
|
408
|
+
const newSupabase = createMockSupabaseClient();
|
|
409
|
+
const newOnIdleLogout = vi.fn();
|
|
410
|
+
|
|
411
|
+
inactivityService.updateDependencies(
|
|
412
|
+
newSupabase as any,
|
|
413
|
+
newUser,
|
|
414
|
+
newSession,
|
|
415
|
+
600000, // 10 minutes
|
|
416
|
+
120000, // 2 minutes warning
|
|
417
|
+
newOnIdleLogout
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Verify state was updated by checking if notifications occurred
|
|
421
|
+
const subscriber = vi.fn();
|
|
422
|
+
inactivityService.subscribe(subscriber);
|
|
423
|
+
|
|
424
|
+
inactivityService.setShowInactivityWarning(true);
|
|
425
|
+
expect(subscriber).toHaveBeenCalled();
|
|
426
|
+
|
|
427
|
+
inactivityService.cleanup();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should update dependencies with partial parameters', () => {
|
|
431
|
+
const newUser = { id: 'user-2', email: 'new@example.com' };
|
|
432
|
+
const newSession = { access_token: 'new-token', user: newUser };
|
|
433
|
+
const newSupabase = createMockSupabaseClient();
|
|
434
|
+
|
|
435
|
+
inactivityService.updateDependencies(
|
|
436
|
+
newSupabase as any,
|
|
437
|
+
newUser,
|
|
438
|
+
newSession
|
|
439
|
+
// Omit optional parameters
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Should not throw error
|
|
443
|
+
expect(inactivityService.getIsTracking()).toBeDefined();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('Initialization and Setup', () => {
|
|
448
|
+
it('should setup inactivity tracker on initialization when user and session available', async () => {
|
|
449
|
+
await inactivityService.initialize();
|
|
450
|
+
|
|
451
|
+
// After initialization, tracker should be set up if user and session are available
|
|
452
|
+
// The actual tracking state depends on setupEventHandlers being called
|
|
453
|
+
expect(inactivityService.getIsTracking()).toBeDefined();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should handle setupInactivityTracker failure gracefully', async () => {
|
|
457
|
+
// Mock the import to fail
|
|
458
|
+
vi.doMock('../../hooks/useInactivityTracker', () => {
|
|
459
|
+
throw new Error('Import failed');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const service = new InactivityService(
|
|
463
|
+
mockSupabase as any,
|
|
464
|
+
mockUser,
|
|
465
|
+
mockSession,
|
|
466
|
+
300000,
|
|
467
|
+
60000,
|
|
468
|
+
mockOnIdleLogout
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Should not throw error
|
|
472
|
+
await expect(service.initialize()).resolves.toBeUndefined();
|
|
473
|
+
|
|
474
|
+
service.cleanup();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should handle window undefined environment', async () => {
|
|
478
|
+
const originalWindow = global.window;
|
|
479
|
+
// @ts-ignore - for testing
|
|
480
|
+
delete global.window;
|
|
481
|
+
|
|
482
|
+
const service = new InactivityService(
|
|
483
|
+
mockSupabase as any,
|
|
484
|
+
mockUser,
|
|
485
|
+
mockSession,
|
|
486
|
+
300000,
|
|
487
|
+
60000,
|
|
488
|
+
mockOnIdleLogout
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
await service.initialize();
|
|
492
|
+
|
|
493
|
+
// Should handle gracefully without window
|
|
494
|
+
expect(service.getIsTracking()).toBeDefined();
|
|
495
|
+
|
|
496
|
+
global.window = originalWindow;
|
|
497
|
+
service.cleanup();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should log production warning when enabled', async () => {
|
|
501
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
502
|
+
const originalMode = import.meta.env.MODE;
|
|
503
|
+
(import.meta.env as any).MODE = 'production';
|
|
504
|
+
|
|
505
|
+
const service = new InactivityService(
|
|
506
|
+
mockSupabase as any,
|
|
507
|
+
mockUser,
|
|
508
|
+
mockSession,
|
|
509
|
+
300000,
|
|
510
|
+
60000,
|
|
511
|
+
mockOnIdleLogout
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
await service.initialize();
|
|
515
|
+
|
|
516
|
+
// Should log warning in production
|
|
517
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
518
|
+
expect.stringContaining('[InactivityService] Inactivity feature enabled in production')
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
(import.meta.env as any).MODE = originalMode;
|
|
522
|
+
consoleWarnSpy.mockRestore();
|
|
523
|
+
service.cleanup();
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('Event Handlers', () => {
|
|
528
|
+
it('should handle activity events and reset timers', async () => {
|
|
529
|
+
await inactivityService.initialize();
|
|
530
|
+
|
|
531
|
+
// Simulate activity event
|
|
532
|
+
const mousedownEvent = new MouseEvent('mousedown', { bubbles: true });
|
|
533
|
+
document.dispatchEvent(mousedownEvent);
|
|
534
|
+
|
|
535
|
+
// Activity should reset timers
|
|
536
|
+
// Note: In test environment, timers may not work exactly as in production
|
|
537
|
+
// but we can verify the event handlers are set up
|
|
538
|
+
expect(inactivityService.getIsTracking()).toBeDefined();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should cleanup event listeners on cleanup', async () => {
|
|
542
|
+
await inactivityService.initialize();
|
|
543
|
+
|
|
544
|
+
// Wait a bit to ensure event handlers are set up
|
|
545
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
546
|
+
|
|
547
|
+
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
|
548
|
+
|
|
549
|
+
inactivityService.cleanup();
|
|
550
|
+
|
|
551
|
+
// Should remove event listeners if they were added
|
|
552
|
+
// Note: In some test environments, event listeners may not be added if window is not properly set up
|
|
553
|
+
// The important thing is that cleanup doesn't throw an error
|
|
554
|
+
expect(() => inactivityService.cleanup()).not.toThrow();
|
|
555
|
+
removeEventListenerSpy.mockRestore();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
describe('Edge Cases', () => {
|
|
560
|
+
it('should handle idle logout when onIdleLogout is null', async () => {
|
|
561
|
+
const serviceWithoutCallback = new InactivityService(
|
|
562
|
+
mockSupabase as any,
|
|
563
|
+
mockUser,
|
|
564
|
+
mockSession,
|
|
565
|
+
300000,
|
|
566
|
+
60000,
|
|
567
|
+
null as any
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
mockSupabase.auth.signOut.mockResolvedValue({ error: null });
|
|
571
|
+
|
|
572
|
+
// Should not throw error
|
|
573
|
+
await expect(serviceWithoutCallback.handleIdleLogout()).resolves.toBeUndefined();
|
|
574
|
+
|
|
575
|
+
serviceWithoutCallback.cleanup();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should handle idle logout when Supabase client is null', async () => {
|
|
579
|
+
const serviceWithoutSupabase = new InactivityService(
|
|
580
|
+
null as any,
|
|
581
|
+
mockUser,
|
|
582
|
+
mockSession,
|
|
583
|
+
300000,
|
|
584
|
+
60000,
|
|
585
|
+
mockOnIdleLogout
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Should not throw error
|
|
589
|
+
await expect(serviceWithoutSupabase.handleIdleLogout()).resolves.toBeUndefined();
|
|
590
|
+
|
|
591
|
+
serviceWithoutSupabase.cleanup();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should handle resetActivity when inactivityTracker exists', () => {
|
|
595
|
+
inactivityService.setIsTracking(true);
|
|
596
|
+
|
|
597
|
+
// Reset should clear all state
|
|
598
|
+
inactivityService.resetActivity();
|
|
599
|
+
|
|
600
|
+
expect(inactivityService.getShowInactivityWarning()).toBe(false);
|
|
601
|
+
expect(inactivityService.getIsIdle()).toBe(false);
|
|
602
|
+
expect(inactivityService.getShowWarning()).toBe(false);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should handle startTracking when inactivityTracker exists', () => {
|
|
606
|
+
inactivityService.startTracking();
|
|
607
|
+
|
|
608
|
+
expect(inactivityService.getIsTracking()).toBe(true);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should handle stopTracking when inactivityTracker exists', () => {
|
|
612
|
+
inactivityService.startTracking();
|
|
613
|
+
expect(inactivityService.getIsTracking()).toBe(true);
|
|
614
|
+
|
|
615
|
+
inactivityService.stopTracking();
|
|
616
|
+
|
|
617
|
+
expect(inactivityService.getIsTracking()).toBe(false);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
403
620
|
});
|