@jmruthers/pace-core 0.5.118 → 0.5.120

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.
Files changed (181) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-DGZDJUYM.js} +7 -7
  2. package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
  3. package/dist/{chunk-7OTQLFVI.js → chunk-B4GZ2BXO.js} +3 -3
  4. package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
  5. package/dist/chunk-BHWIUEYH.js.map +1 -0
  6. package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
  7. package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
  8. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  9. package/dist/{chunk-2GJ5GL77.js → chunk-GKHF54DI.js} +2 -2
  10. package/dist/chunk-GKHF54DI.js.map +1 -0
  11. package/dist/{chunk-UKZWNQMB.js → chunk-HFBOFZ3Z.js} +5 -18
  12. package/dist/chunk-HFBOFZ3Z.js.map +1 -0
  13. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  14. package/dist/{chunk-2LM4QQGH.js → chunk-QPI2CCBA.js} +9 -9
  15. package/dist/chunk-QPI2CCBA.js.map +1 -0
  16. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  17. package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
  18. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  19. package/dist/components.d.ts +1 -1
  20. package/dist/components.js +9 -9
  21. package/dist/hooks.d.ts +1 -1
  22. package/dist/hooks.js +8 -8
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +12 -12
  25. package/dist/providers.js +2 -2
  26. package/dist/rbac/index.js +7 -7
  27. package/dist/{useToast-Cs_g32bg.d.ts → useToast-C8gR5ir4.d.ts} +2 -2
  28. package/dist/utils.js +1 -1
  29. package/docs/api/classes/ColumnFactory.md +1 -1
  30. package/docs/api/classes/ErrorBoundary.md +1 -1
  31. package/docs/api/classes/InvalidScopeError.md +1 -1
  32. package/docs/api/classes/MissingUserContextError.md +1 -1
  33. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  34. package/docs/api/classes/PermissionDeniedError.md +1 -1
  35. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  36. package/docs/api/classes/RBACAuditManager.md +1 -1
  37. package/docs/api/classes/RBACCache.md +1 -1
  38. package/docs/api/classes/RBACEngine.md +1 -1
  39. package/docs/api/classes/RBACError.md +1 -1
  40. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  41. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  42. package/docs/api/classes/StorageUtils.md +1 -1
  43. package/docs/api/enums/FileCategory.md +1 -1
  44. package/docs/api/interfaces/AggregateConfig.md +1 -1
  45. package/docs/api/interfaces/ButtonProps.md +1 -1
  46. package/docs/api/interfaces/CardProps.md +1 -1
  47. package/docs/api/interfaces/ColorPalette.md +1 -1
  48. package/docs/api/interfaces/ColorShade.md +1 -1
  49. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  50. package/docs/api/interfaces/DataRecord.md +1 -1
  51. package/docs/api/interfaces/DataTableAction.md +1 -1
  52. package/docs/api/interfaces/DataTableColumn.md +1 -1
  53. package/docs/api/interfaces/DataTableProps.md +1 -1
  54. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  55. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  56. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  57. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  58. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  59. package/docs/api/interfaces/FileMetadata.md +1 -1
  60. package/docs/api/interfaces/FileReference.md +1 -1
  61. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  62. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  63. package/docs/api/interfaces/FileUploadProps.md +1 -1
  64. package/docs/api/interfaces/FooterProps.md +1 -1
  65. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  66. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  67. package/docs/api/interfaces/InputProps.md +1 -1
  68. package/docs/api/interfaces/LabelProps.md +1 -1
  69. package/docs/api/interfaces/LoginFormProps.md +1 -1
  70. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  71. package/docs/api/interfaces/NavigationContextType.md +1 -1
  72. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  73. package/docs/api/interfaces/NavigationItem.md +1 -1
  74. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  76. package/docs/api/interfaces/Organisation.md +1 -1
  77. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  78. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  79. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  80. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  81. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  82. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  83. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  84. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  85. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  86. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  87. package/docs/api/interfaces/PaletteData.md +1 -1
  88. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  89. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  90. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  91. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  92. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  93. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  94. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  95. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  96. package/docs/api/interfaces/RBACConfig.md +1 -1
  97. package/docs/api/interfaces/RBACLogger.md +1 -1
  98. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  99. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  100. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  101. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  102. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  103. package/docs/api/interfaces/RouteConfig.md +1 -1
  104. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  105. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  106. package/docs/api/interfaces/StorageConfig.md +1 -1
  107. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  108. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  109. package/docs/api/interfaces/StorageListOptions.md +1 -1
  110. package/docs/api/interfaces/StorageListResult.md +1 -1
  111. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  112. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  113. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  114. package/docs/api/interfaces/StyleImport.md +1 -1
  115. package/docs/api/interfaces/SwitchProps.md +1 -1
  116. package/docs/api/interfaces/ToastActionElement.md +1 -1
  117. package/docs/api/interfaces/ToastProps.md +1 -1
  118. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  119. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  120. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  121. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  123. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  124. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  125. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  127. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  128. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  129. package/docs/api/interfaces/UserEventAccess.md +1 -1
  130. package/docs/api/interfaces/UserMenuProps.md +1 -1
  131. package/docs/api/interfaces/UserProfile.md +1 -1
  132. package/docs/api/modules.md +2 -2
  133. package/package.json +1 -1
  134. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  135. package/src/components/DataTable/components/DataTableCore.tsx +5 -0
  136. package/src/components/DataTable/components/EditableRow.tsx +9 -18
  137. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +616 -9
  138. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  139. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  140. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  141. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  142. package/src/components/Toast/Toast.tsx +1 -1
  143. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  144. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  145. package/src/hooks/__tests__/useEvents.unit.test.ts +251 -0
  146. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  147. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  148. package/src/hooks/__tests__/useFocusManagement.unit.test.ts +19 -9
  149. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  150. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  151. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  152. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  153. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +661 -0
  154. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  155. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +371 -0
  156. package/src/hooks/__tests__/useToast.unit.test.tsx +449 -30
  157. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  158. package/src/hooks/useToast.ts +4 -4
  159. package/src/rbac/audit-enhanced.ts +339 -0
  160. package/src/services/EventService.ts +1 -0
  161. package/src/services/__tests__/AuthService.test.ts +473 -0
  162. package/src/services/__tests__/EventService.test.ts +390 -0
  163. package/src/services/__tests__/InactivityService.test.ts +217 -0
  164. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  165. package/src/styles/core.css +1 -0
  166. package/dist/chunk-2GJ5GL77.js.map +0 -1
  167. package/dist/chunk-2LM4QQGH.js.map +0 -1
  168. package/dist/chunk-KA3PSVNV.js.map +0 -1
  169. package/dist/chunk-UKZWNQMB.js.map +0 -1
  170. package/src/components/DataTable/utils/debugTools.ts +0 -609
  171. package/src/rbac/testing/index.tsx +0 -340
  172. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-DGZDJUYM.js.map} +0 -0
  173. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  174. /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
  175. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  176. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  177. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  178. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  179. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  180. /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  181. /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
  });