@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.
Files changed (166) hide show
  1. package/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
  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-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
  9. package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
  10. package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
  11. package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
  12. package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
  13. package/dist/{chunk-HIWXXDXO.js → chunk-TDNI6ZWL.js} +5 -5
  14. package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
  15. package/dist/components.js +8 -8
  16. package/dist/hooks.js +7 -7
  17. package/dist/index.js +11 -11
  18. package/dist/providers.js +2 -2
  19. package/dist/rbac/index.js +7 -7
  20. package/dist/utils.js +1 -1
  21. package/docs/api/classes/ColumnFactory.md +1 -1
  22. package/docs/api/classes/ErrorBoundary.md +1 -1
  23. package/docs/api/classes/InvalidScopeError.md +1 -1
  24. package/docs/api/classes/MissingUserContextError.md +1 -1
  25. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  26. package/docs/api/classes/PermissionDeniedError.md +1 -1
  27. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  28. package/docs/api/classes/RBACAuditManager.md +1 -1
  29. package/docs/api/classes/RBACCache.md +1 -1
  30. package/docs/api/classes/RBACEngine.md +1 -1
  31. package/docs/api/classes/RBACError.md +1 -1
  32. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  33. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  34. package/docs/api/classes/StorageUtils.md +1 -1
  35. package/docs/api/enums/FileCategory.md +1 -1
  36. package/docs/api/interfaces/AggregateConfig.md +1 -1
  37. package/docs/api/interfaces/ButtonProps.md +1 -1
  38. package/docs/api/interfaces/CardProps.md +1 -1
  39. package/docs/api/interfaces/ColorPalette.md +1 -1
  40. package/docs/api/interfaces/ColorShade.md +1 -1
  41. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  42. package/docs/api/interfaces/DataRecord.md +1 -1
  43. package/docs/api/interfaces/DataTableAction.md +1 -1
  44. package/docs/api/interfaces/DataTableColumn.md +1 -1
  45. package/docs/api/interfaces/DataTableProps.md +1 -1
  46. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  47. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  48. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  49. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  50. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  51. package/docs/api/interfaces/FileMetadata.md +1 -1
  52. package/docs/api/interfaces/FileReference.md +1 -1
  53. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  54. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  55. package/docs/api/interfaces/FileUploadProps.md +1 -1
  56. package/docs/api/interfaces/FooterProps.md +1 -1
  57. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  58. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  59. package/docs/api/interfaces/InputProps.md +1 -1
  60. package/docs/api/interfaces/LabelProps.md +1 -1
  61. package/docs/api/interfaces/LoginFormProps.md +1 -1
  62. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  63. package/docs/api/interfaces/NavigationContextType.md +1 -1
  64. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  65. package/docs/api/interfaces/NavigationItem.md +1 -1
  66. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  67. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  68. package/docs/api/interfaces/Organisation.md +1 -1
  69. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  70. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  71. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  72. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  73. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  74. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  75. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  76. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  77. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  78. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  79. package/docs/api/interfaces/PaletteData.md +1 -1
  80. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  81. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  83. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  84. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  86. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  87. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  88. package/docs/api/interfaces/RBACConfig.md +1 -1
  89. package/docs/api/interfaces/RBACLogger.md +1 -1
  90. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  91. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  92. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  93. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  94. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  95. package/docs/api/interfaces/RouteConfig.md +1 -1
  96. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  97. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  98. package/docs/api/interfaces/StorageConfig.md +1 -1
  99. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  100. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  101. package/docs/api/interfaces/StorageListOptions.md +1 -1
  102. package/docs/api/interfaces/StorageListResult.md +1 -1
  103. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  104. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  105. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  106. package/docs/api/interfaces/StyleImport.md +1 -1
  107. package/docs/api/interfaces/SwitchProps.md +1 -1
  108. package/docs/api/interfaces/ToastActionElement.md +1 -1
  109. package/docs/api/interfaces/ToastProps.md +1 -1
  110. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  111. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  112. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  113. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  114. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  115. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  116. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  117. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  119. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  120. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  121. package/docs/api/interfaces/UserEventAccess.md +1 -1
  122. package/docs/api/interfaces/UserMenuProps.md +1 -1
  123. package/docs/api/interfaces/UserProfile.md +1 -1
  124. package/docs/api/modules.md +2 -2
  125. package/package.json +1 -1
  126. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
  127. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
  128. package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
  129. package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
  130. package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
  131. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
  132. package/src/hooks/__tests__/index.unit.test.ts +223 -0
  133. package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
  134. package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
  135. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
  136. package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
  137. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
  138. package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
  139. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
  140. package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
  141. package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
  142. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
  143. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
  144. package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
  145. package/src/hooks/useSecureDataAccess.test.ts +1 -0
  146. package/src/rbac/audit-enhanced.ts +339 -0
  147. package/src/services/EventService.ts +1 -0
  148. package/src/services/__tests__/AuthService.test.ts +473 -0
  149. package/src/services/__tests__/EventService.test.ts +390 -0
  150. package/src/services/__tests__/InactivityService.test.ts +217 -0
  151. package/src/services/__tests__/OrganisationService.test.ts +371 -0
  152. package/dist/chunk-KA3PSVNV.js.map +0 -1
  153. package/src/components/DataTable/utils/debugTools.ts +0 -609
  154. package/src/rbac/testing/index.tsx +0 -340
  155. /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
  156. /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
  157. /package/dist/{chunk-7OTQLFVI.js.map → chunk-B4GZ2BXO.js.map} +0 -0
  158. /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
  159. /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
  160. /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
  161. /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
  162. /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
  163. /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
  164. /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
  165. /package/dist/{chunk-HIWXXDXO.js.map → chunk-TDNI6ZWL.js.map} +0 -0
  166. /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
@@ -657,4 +657,375 @@ describe('OrganisationService', () => {
657
657
  expect(organisationService.getSelectedOrganisation()).toEqual(mockOrganisation);
658
658
  });
659
659
  });
660
+
661
+ describe('Dependency Updates', () => {
662
+ it('should update dependencies when user changes', () => {
663
+ const newUser = { id: 'user-2', email: 'new@example.com' };
664
+ const newSession = { access_token: 'new-token', user: newUser };
665
+
666
+ organisationService.updateDependencies(newUser, newSession);
667
+
668
+ const deps = organisationService.getDependencies();
669
+ expect(deps.user).toEqual(newUser);
670
+ expect(deps.session).toEqual(newSession);
671
+ });
672
+
673
+ it('should reset initialization state when user logs out', () => {
674
+ // Set up authenticated state
675
+ organisationService.updateDependencies(mockUser, mockSession);
676
+
677
+ // Log out
678
+ organisationService.updateDependencies(null, null);
679
+
680
+ // Should allow re-initialization when user logs back in
681
+ const deps = organisationService.getDependencies();
682
+ expect(deps.user).toBeNull();
683
+ expect(deps.session).toBeNull();
684
+ });
685
+ });
686
+
687
+ describe('Organisation Hierarchy', () => {
688
+ it('should build organisation hierarchy', () => {
689
+ const orgs = [mockOrganisation, mockOrganisation2];
690
+ const hierarchy = organisationService.buildOrganisationHierarchy(orgs);
691
+
692
+ expect(hierarchy).toBeDefined();
693
+ expect(Array.isArray(hierarchy)).toBe(true);
694
+ // Should return root organisations (those without parent_id)
695
+ expect(hierarchy.length).toBeGreaterThanOrEqual(0);
696
+ });
697
+
698
+ it('should handle empty organisation list in hierarchy', () => {
699
+ const hierarchy = organisationService.buildOrganisationHierarchy([]);
700
+
701
+ expect(hierarchy).toEqual([]);
702
+ });
703
+ });
704
+
705
+ describe('Database Context', () => {
706
+ it('should handle database context setting timeout', async () => {
707
+ vi.useFakeTimers();
708
+
709
+ const { setOrganisationContext } = await import('../../utils/organisationContext');
710
+ vi.mocked(setOrganisationContext).mockImplementation(() => {
711
+ return new Promise(() => {
712
+ // Never resolves, will timeout after 5 seconds
713
+ });
714
+ });
715
+
716
+ organisationService.setSelectedOrganisation(mockOrganisation);
717
+
718
+ // Advance timers past the 5 second timeout
719
+ await vi.advanceTimersByTimeAsync(6000);
720
+
721
+ // Wait a bit for async operations to complete
722
+ await vi.runAllTimersAsync();
723
+
724
+ // Context ready should be set even on timeout (non-critical operation)
725
+ // The service sets _isContextReady to true even on error
726
+ expect(organisationService.isContextReady()).toBe(true);
727
+
728
+ vi.useRealTimers();
729
+ });
730
+
731
+ it('should handle database context setting error', async () => {
732
+ const { setOrganisationContext } = await import('../../utils/organisationContext');
733
+ vi.mocked(setOrganisationContext).mockRejectedValue(new Error('Database error'));
734
+
735
+ organisationService.setSelectedOrganisation(mockOrganisation);
736
+
737
+ // Should handle error gracefully
738
+ await new Promise(resolve => setTimeout(resolve, 100));
739
+
740
+ // Context ready should be set even on error
741
+ expect(organisationService.isContextReady()).toBe(true);
742
+ });
743
+
744
+ it('should handle missing Supabase client when setting context', async () => {
745
+ const serviceWithoutClient = new OrganisationService(
746
+ null as any,
747
+ mockUser,
748
+ mockSession
749
+ );
750
+
751
+ serviceWithoutClient.setSelectedOrganisation(mockOrganisation);
752
+
753
+ // Should handle gracefully
754
+ await new Promise(resolve => setTimeout(resolve, 100));
755
+ expect(serviceWithoutClient.isContextReady()).toBe(false);
756
+ });
757
+
758
+ it('should handle missing session when setting context', async () => {
759
+ const serviceWithoutSession = new OrganisationService(
760
+ mockSupabase as any,
761
+ mockUser,
762
+ null
763
+ );
764
+
765
+ serviceWithoutSession.setSelectedOrganisation(mockOrganisation);
766
+
767
+ // Should handle gracefully
768
+ await new Promise(resolve => setTimeout(resolve, 100));
769
+ expect(serviceWithoutSession.isContextReady()).toBe(false);
770
+ });
771
+ });
772
+
773
+ describe('Load User Organisations Edge Cases', () => {
774
+ it('should handle RPC timeout with fallback query', async () => {
775
+ // Mock RPC to timeout
776
+ mockSupabase.rpc.mockImplementation(() => {
777
+ return new Promise((_, reject) => {
778
+ setTimeout(() => reject(new Error('RPC call timeout after 10 seconds')), 100);
779
+ });
780
+ });
781
+
782
+ // Mock fallback query
783
+ mockSupabase.from.mockImplementation((table: string) => {
784
+ if (table === 'rbac_organisation_roles') {
785
+ return {
786
+ select: vi.fn().mockReturnValue({
787
+ eq: vi.fn().mockReturnValue({
788
+ eq: vi.fn().mockReturnValue({
789
+ is: vi.fn().mockReturnValue({
790
+ in: vi.fn().mockResolvedValue({
791
+ data: [mockMembership],
792
+ error: null
793
+ })
794
+ })
795
+ })
796
+ })
797
+ })
798
+ };
799
+ }
800
+ if (table === 'organisations') {
801
+ return {
802
+ select: vi.fn().mockResolvedValue({
803
+ data: [mockOrganisation],
804
+ error: null
805
+ })
806
+ };
807
+ }
808
+ return {
809
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
810
+ };
811
+ });
812
+
813
+ await organisationService.initialize();
814
+
815
+ // Should handle timeout and use fallback
816
+ expect(mockSupabase.from).toHaveBeenCalled();
817
+ });
818
+
819
+ it('should handle abort signal during RPC call', async () => {
820
+ const abortController = new AbortController();
821
+ abortController.abort();
822
+
823
+ mockSupabase.rpc.mockImplementation(() => {
824
+ return Promise.reject(new Error('Request aborted'));
825
+ });
826
+
827
+ await organisationService.initialize();
828
+
829
+ // Should handle abort gracefully
830
+ expect(organisationService.getError() || organisationService.getOrganisations()).toBeDefined();
831
+ });
832
+
833
+ it('should prevent duplicate loads when already loading', async () => {
834
+ // Set loading state
835
+ (organisationService as any).isLoadingRef = true;
836
+
837
+ // Call refreshOrganisations which internally calls loadUserOrganisations
838
+ await organisationService.refreshOrganisations();
839
+
840
+ // Should handle duplicate load prevention
841
+ expect(organisationService.isLoading()).toBeDefined();
842
+ });
843
+
844
+ it('should prevent rapid retries', async () => {
845
+ (organisationService as any).lastLoadTimeRef = Date.now();
846
+
847
+ // Call refreshOrganisations which internally calls loadUserOrganisations
848
+ await organisationService.refreshOrganisations();
849
+
850
+ // Should skip if too soon since last load
851
+ expect(organisationService.isLoading()).toBeDefined();
852
+ });
853
+
854
+ it('should handle invalid organisation IDs in memberships', async () => {
855
+ mockSupabase.rpc.mockResolvedValue({
856
+ data: [{ ...mockMembership, organisation_id: '' }], // Empty ID
857
+ error: null
858
+ });
859
+
860
+ mockSupabase.from.mockImplementation((table: string) => {
861
+ if (table === 'organisations') {
862
+ return {
863
+ select: vi.fn().mockResolvedValue({
864
+ data: [],
865
+ error: null
866
+ })
867
+ };
868
+ }
869
+ return {
870
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
871
+ };
872
+ });
873
+
874
+ await organisationService.initialize();
875
+
876
+ expect(organisationService.getError()).toBeDefined();
877
+ expect(organisationService.getError()?.message).toContain('No valid organisation IDs');
878
+ });
879
+
880
+ it('should handle non-UUID organisation IDs', async () => {
881
+ mockSupabase.rpc.mockResolvedValue({
882
+ data: [{ ...mockMembership, organisation_id: 'invalid-id' }],
883
+ error: null
884
+ });
885
+
886
+ mockSupabase.from.mockImplementation((table: string) => {
887
+ if (table === 'organisations') {
888
+ return {
889
+ select: vi.fn().mockResolvedValue({
890
+ data: [],
891
+ error: null
892
+ })
893
+ };
894
+ }
895
+ return {
896
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
897
+ };
898
+ });
899
+
900
+ await organisationService.initialize();
901
+
902
+ expect(organisationService.getError()).toBeDefined();
903
+ });
904
+
905
+ it('should clear cached data on error', async () => {
906
+ mockSupabase.rpc.mockRejectedValue(new Error('Network error'));
907
+
908
+ localStorage.setItem('pace-core-selected-organisation', JSON.stringify(mockOrganisation));
909
+ localStorage.setItem('pace-core-organisation-context', 'test');
910
+
911
+ await organisationService.initialize();
912
+
913
+ // Should clear cached data
914
+ expect(localStorage.getItem('pace-core-selected-organisation')).toBeNull();
915
+ expect(localStorage.getItem('pace-core-organisation-context')).toBeNull();
916
+ });
917
+
918
+ it('should handle fallback query failure', async () => {
919
+ // Mock RPC to timeout
920
+ mockSupabase.rpc.mockImplementation(() => {
921
+ return Promise.reject(new Error('RPC call timeout after 10 seconds'));
922
+ });
923
+
924
+ // Mock fallback query to also fail
925
+ mockSupabase.from.mockImplementation((table: string) => {
926
+ if (table === 'rbac_organisation_roles') {
927
+ return {
928
+ select: vi.fn().mockReturnValue({
929
+ eq: vi.fn().mockReturnValue({
930
+ eq: vi.fn().mockReturnValue({
931
+ is: vi.fn().mockReturnValue({
932
+ in: vi.fn().mockRejectedValue(new Error('Fallback query failed'))
933
+ })
934
+ })
935
+ })
936
+ })
937
+ };
938
+ }
939
+ return {
940
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
941
+ };
942
+ });
943
+
944
+ await organisationService.initialize();
945
+
946
+ // Should handle fallback failure
947
+ expect(organisationService.getError()).toBeDefined();
948
+ });
949
+
950
+ it('should handle invalid persisted organisation ID format', async () => {
951
+ // Set invalid persisted organisation
952
+ localStorage.setItem('pace-core-selected-organisation', JSON.stringify({ id: '' }));
953
+
954
+ mockSupabase.rpc.mockResolvedValue({
955
+ data: [mockMembership],
956
+ error: null
957
+ });
958
+
959
+ mockSupabase.from.mockImplementation((table: string) => {
960
+ if (table === 'organisations') {
961
+ return {
962
+ select: vi.fn().mockResolvedValue({
963
+ data: [mockOrganisation],
964
+ error: null
965
+ })
966
+ };
967
+ }
968
+ return {
969
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
970
+ };
971
+ });
972
+
973
+ await organisationService.initialize();
974
+
975
+ // Should clear invalid persisted org
976
+ expect(localStorage.getItem('pace-core-selected-organisation')).toBeNull();
977
+ });
978
+
979
+ it('should handle corrupted persisted organisation JSON', async () => {
980
+ localStorage.setItem('pace-core-selected-organisation', 'invalid-json');
981
+
982
+ mockSupabase.rpc.mockResolvedValue({
983
+ data: [mockMembership],
984
+ error: null
985
+ });
986
+
987
+ mockSupabase.from.mockImplementation((table: string) => {
988
+ if (table === 'organisations') {
989
+ return {
990
+ select: vi.fn().mockResolvedValue({
991
+ data: [mockOrganisation],
992
+ error: null
993
+ })
994
+ };
995
+ }
996
+ return {
997
+ select: vi.fn().mockResolvedValue({ data: [], error: null })
998
+ };
999
+ });
1000
+
1001
+ await organisationService.initialize();
1002
+
1003
+ // Should clear corrupted JSON
1004
+ expect(localStorage.getItem('pace-core-selected-organisation')).toBeNull();
1005
+ });
1006
+ });
1007
+
1008
+ describe('Context Ready State', () => {
1009
+ it('should return false when context is not ready', () => {
1010
+ expect(organisationService.isContextReady()).toBe(false);
1011
+ });
1012
+
1013
+ it('should return false when organisation is not selected', () => {
1014
+ expect(organisationService.hasValidOrganisationContext()).toBe(false);
1015
+ });
1016
+
1017
+ it('should return false when loading', () => {
1018
+ organisationService.setSelectedOrganisation(mockOrganisation);
1019
+ (organisationService as any)._isLoading = true;
1020
+
1021
+ expect(organisationService.hasValidOrganisationContext()).toBe(false);
1022
+ });
1023
+
1024
+ it('should return false when error exists', () => {
1025
+ organisationService.setSelectedOrganisation(mockOrganisation);
1026
+ (organisationService as any)._error = new Error('Test error');
1027
+
1028
+ expect(organisationService.hasValidOrganisationContext()).toBe(false);
1029
+ });
1030
+ });
660
1031
  });