Swift Testing for Unit Tests PART 1 (#5119)
* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment more tests migration Cleaned the code manually to establish some good patterns more code improvements some more code improvements removed empty tests update project * more pr suggestions and cleanups * removed the TestSetup pattern * fixing claude not reusing tests * pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
--commas inline
|
||||
--ifdef no-indent
|
||||
--indent 4
|
||||
--nospaceoperators ...,..<
|
||||
--stripunusedargs closure-only
|
||||
--trimwhitespace nonblank-lines
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; };
|
||||
065EAB39F3F3AB4F6BD2A362 /* AppLockSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */; };
|
||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
|
||||
06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */; };
|
||||
06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
|
||||
06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; };
|
||||
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; };
|
||||
@@ -175,7 +174,6 @@
|
||||
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */; };
|
||||
1B5B30839656AE2F957C6B1E /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; };
|
||||
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; };
|
||||
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; };
|
||||
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; };
|
||||
1BEADA694AC53ABB8B459F9A /* LeaveSpaceRoomDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3797A2325BE44FFB478BE9 /* LeaveSpaceRoomDetailsCell.swift */; };
|
||||
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */; };
|
||||
@@ -310,7 +308,6 @@
|
||||
3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; };
|
||||
35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; };
|
||||
36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; };
|
||||
366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; };
|
||||
3684AD01C5FCB7616B28F629 /* TimelineMediaPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDE60FEE95039CCCEEEE3B0 /* TimelineMediaPreviewController.swift */; };
|
||||
36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; };
|
||||
369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */; };
|
||||
@@ -519,7 +516,6 @@
|
||||
5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */; };
|
||||
5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; };
|
||||
5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; };
|
||||
5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; };
|
||||
5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; };
|
||||
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
|
||||
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; };
|
||||
@@ -682,7 +678,6 @@
|
||||
763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; };
|
||||
7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.swift */; };
|
||||
767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; };
|
||||
7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; };
|
||||
76C874243A8C440D6CF7B344 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; };
|
||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
||||
77574A519A4E484880053EAD /* IdentityConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */; };
|
||||
@@ -744,7 +739,6 @@
|
||||
804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; };
|
||||
80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; };
|
||||
80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */; };
|
||||
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36C0A6D59717193F49EA986 /* UserSessionTests.swift */; };
|
||||
81CFE6FE42DF26BBCEDC7FF2 /* JoinCallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */; };
|
||||
81D4E550668B230A63B26CFB /* SpacesScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */; };
|
||||
8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; };
|
||||
@@ -768,7 +762,6 @@
|
||||
85BD82E144AB99518A57DDEC /* preview_avatar_room.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */; };
|
||||
85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; };
|
||||
864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5281C5CDC4A712265A0B5FBF /* PollRoomTimelineItem.swift */; };
|
||||
864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */; };
|
||||
8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */; };
|
||||
865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; };
|
||||
86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; };
|
||||
@@ -806,7 +799,6 @@
|
||||
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; };
|
||||
8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; };
|
||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; };
|
||||
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */; };
|
||||
8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260004737C573A56FA01E86E /* Encodable.swift */; };
|
||||
8B408C574E35E1C9B43A50CE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */; };
|
||||
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; };
|
||||
@@ -1417,6 +1409,7 @@
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
|
||||
F71C2B24AFB566119ACCDDA1 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
|
||||
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
|
||||
F769F921D7823C2F1CBB5047 /* DeferredFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */; };
|
||||
F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; };
|
||||
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; };
|
||||
F7932A3F075B0D3F24DEECB5 /* VoiceMessagePreviewComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */; };
|
||||
@@ -1827,7 +1820,6 @@
|
||||
2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = "<group>"; };
|
||||
2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
||||
2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; };
|
||||
2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = "<group>"; };
|
||||
@@ -1906,7 +1898,6 @@
|
||||
3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = "<group>"; };
|
||||
3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = "<group>"; };
|
||||
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreen.swift; sourceTree = "<group>"; };
|
||||
406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; };
|
||||
407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; };
|
||||
@@ -2140,10 +2131,8 @@
|
||||
6C9651CD1066F239C7739240 /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = "<group>"; };
|
||||
6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
|
||||
6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsScreenIdentifier.swift; sourceTree = "<group>"; };
|
||||
6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatViewModelTests.swift; sourceTree = "<group>"; };
|
||||
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
|
||||
6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = "<group>"; };
|
||||
6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
|
||||
@@ -2611,7 +2600,6 @@
|
||||
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
|
||||
C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = "<group>"; };
|
||||
C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = "<group>"; };
|
||||
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
|
||||
@@ -2627,6 +2615,7 @@
|
||||
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; };
|
||||
C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = "<group>"; };
|
||||
C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -2665,7 +2654,6 @@
|
||||
CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -2853,7 +2841,6 @@
|
||||
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
|
||||
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = "<group>"; };
|
||||
EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = "<group>"; };
|
||||
@@ -2883,7 +2870,6 @@
|
||||
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
|
||||
F320003F490B11F808ECC5E9 /* JoinedMembersBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedMembersBadgeView.swift; sourceTree = "<group>"; };
|
||||
F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = "<group>"; };
|
||||
F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||
F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = "<group>"; };
|
||||
@@ -4366,14 +4352,6 @@
|
||||
path = Scripts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53280D2292E6C9C7821773FD /* UserSession */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36C0A6D59717193F49EA986 /* UserSessionTests.swift */,
|
||||
);
|
||||
path = UserSession;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5329E48968EB951235E83DAE /* SessionVerification */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -4760,7 +4738,6 @@
|
||||
240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */,
|
||||
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */,
|
||||
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */,
|
||||
CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */,
|
||||
0328F54E0C3AAEDDF3E05D9D /* ChatsTabFlowCoordinatorTests.swift */,
|
||||
D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */,
|
||||
CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */,
|
||||
@@ -4769,7 +4746,6 @@
|
||||
D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */,
|
||||
2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */,
|
||||
DEBB74427E24AF30CDB131B7 /* DeferredFulfillmentTests.swift */,
|
||||
6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */,
|
||||
906451FB8CF27C628152BF7A /* EditRoomAddressScreenViewModelTests.swift */,
|
||||
7EA2AFF6EB59FE25234D29F3 /* ElementCallServiceTests.swift */,
|
||||
A1087DCC491CD4C027173DDA /* EmojiPickerScreenViewModelTests.swift */,
|
||||
@@ -4783,7 +4759,6 @@
|
||||
DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */,
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */,
|
||||
C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */,
|
||||
6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */,
|
||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
|
||||
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
|
||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
|
||||
@@ -4815,7 +4790,6 @@
|
||||
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */,
|
||||
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */,
|
||||
166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */,
|
||||
EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */,
|
||||
6AE5800184E93CD5E02C6543 /* RoomEventStringBuilderTests.swift */,
|
||||
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */,
|
||||
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */,
|
||||
@@ -4831,10 +4805,7 @@
|
||||
F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */,
|
||||
046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */,
|
||||
B7728AA8046D460145EAC740 /* RoomTests.swift */,
|
||||
2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */,
|
||||
848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */,
|
||||
C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */,
|
||||
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */,
|
||||
0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */,
|
||||
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */,
|
||||
F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */,
|
||||
@@ -4866,7 +4837,6 @@
|
||||
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */,
|
||||
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */,
|
||||
D93C94C30E3135BC9290DE13 /* VoiceMessageRecorderTests.swift */,
|
||||
53280D2292E6C9C7821773FD /* UserSession */,
|
||||
9613851C68D8C01EABFB3569 /* AppLock */,
|
||||
A6AA0A048CAE428A5CA4CBBB /* LayoutTests */,
|
||||
7583EAC171059A86B767209F /* MediaProvider */,
|
||||
@@ -6020,6 +5990,7 @@
|
||||
AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */,
|
||||
127A57D053CE8C87B5EFB089 /* Consumable.swift */,
|
||||
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */,
|
||||
C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */,
|
||||
7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */,
|
||||
6A580295A56B55A856CC4084 /* InfoPlistReader.swift */,
|
||||
6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */,
|
||||
@@ -7635,7 +7606,6 @@
|
||||
CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */,
|
||||
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */,
|
||||
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
|
||||
366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */,
|
||||
4BD5AB54A6982CF19F5CC7C4 /* ChatsTabFlowCoordinatorTests.swift in Sources */,
|
||||
B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */,
|
||||
3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */,
|
||||
@@ -7645,7 +7615,6 @@
|
||||
80F6C8EFCA4564B67F0D34B0 /* DeactivateAccountScreenViewModelTests.swift in Sources */,
|
||||
34390DAE0C574DAD30CCA7D9 /* DeclineAndBlockScreenViewModelTests.swift in Sources */,
|
||||
A583B70939707197B0B21DFC /* DeferredFulfillmentTests.swift in Sources */,
|
||||
864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */,
|
||||
EDB6915EC953BB2A44AA608E /* EditRoomAddressScreenViewModelTests.swift in Sources */,
|
||||
D820B3C223E4C2E77BB2A2BF /* ElementCallServiceTests.swift in Sources */,
|
||||
7AE25D29734267271106D732 /* EmojiPickerScreenViewModelTests.swift in Sources */,
|
||||
@@ -7661,7 +7630,6 @@
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */,
|
||||
BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */,
|
||||
CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */,
|
||||
8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */,
|
||||
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
|
||||
@@ -7700,7 +7668,6 @@
|
||||
D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */,
|
||||
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */,
|
||||
B73E50AF1AB2EB5477E20710 /* RoomDetailsScreenViewModelTests.swift in Sources */,
|
||||
5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */,
|
||||
E591742E509A2A009BF25F9D /* RoomEventStringBuilderTests.swift in Sources */,
|
||||
095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */,
|
||||
4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */,
|
||||
@@ -7716,10 +7683,7 @@
|
||||
6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */,
|
||||
15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */,
|
||||
62811275F1ED9EA55638838E /* RoomTests.swift in Sources */,
|
||||
7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */,
|
||||
EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */,
|
||||
06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */,
|
||||
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */,
|
||||
CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */,
|
||||
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */,
|
||||
89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */,
|
||||
@@ -7752,7 +7716,6 @@
|
||||
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */,
|
||||
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */,
|
||||
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */,
|
||||
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
|
||||
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */,
|
||||
44BDD670FF9095ACE240A3A2 /* VoiceMessageMediaManagerTests.swift in Sources */,
|
||||
A3D7110C1E75E7B4A73BE71C /* VoiceMessageRecorderTests.swift in Sources */,
|
||||
@@ -8036,6 +7999,7 @@
|
||||
0743CF689EBDAAF1CC0B4283 /* DeclineAndBlockScreenViewModel.swift in Sources */,
|
||||
F7DA19B5122AD8FA8F91B753 /* DeclineAndBlockScreenViewModelProtocol.swift in Sources */,
|
||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */,
|
||||
F769F921D7823C2F1CBB5047 /* DeferredFulfillment.swift in Sources */,
|
||||
5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */,
|
||||
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */,
|
||||
6BAE34CFA9821709CFE61E50 /* DeveloperOptionsScreenHook.swift in Sources */,
|
||||
|
||||
242
ElementX/Sources/Other/DeferredFulfillment.swift
Normal file
242
ElementX/Sources/Other/DeferredFulfillment.swift
Normal file
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2023-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
struct DeferredFulfillment<T> {
|
||||
let closure: () async throws -> T
|
||||
|
||||
@discardableResult
|
||||
func fulfill() async throws -> T {
|
||||
try await closure()
|
||||
}
|
||||
}
|
||||
|
||||
struct DeferredFulfillmentError: Error {
|
||||
enum Kind {
|
||||
case noOutput
|
||||
case unexpectedFulfillment
|
||||
}
|
||||
|
||||
let kind: Kind
|
||||
let message: String?
|
||||
|
||||
static func noOutput(message: String?) -> Self {
|
||||
.init(kind: .noOutput, message: message)
|
||||
}
|
||||
|
||||
static func unexpectedFulfillment(message: String?) -> Self {
|
||||
.init(kind: .unexpectedFulfillment, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
|
||||
/// - Parameters:
|
||||
/// - publisher: The publisher to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - until: callback that evaluates outputs until some condition is reached
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
|
||||
func deferFulfillment<P: Publisher>(_ publisher: P,
|
||||
timeout: Duration = .seconds(10),
|
||||
message: String? = nil,
|
||||
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<P.Output> {
|
||||
var result: Result<P.Output, Error>?
|
||||
var hasFulfilled = false
|
||||
|
||||
let cancellable = publisher
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
result = .failure(error)
|
||||
hasFulfilled = true
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { value in
|
||||
if condition(value), !hasFulfilled {
|
||||
result = .success(value)
|
||||
hasFulfilled = true
|
||||
}
|
||||
}
|
||||
|
||||
return DeferredFulfillment<P.Output> {
|
||||
let startTime = ContinuousClock.now
|
||||
|
||||
while !hasFulfilled {
|
||||
await Task.yield()
|
||||
if ContinuousClock.now - startTime >= timeout {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cancellable.cancel()
|
||||
|
||||
guard let unwrappedResult = result else {
|
||||
throw DeferredFulfillmentError.noOutput(message: message)
|
||||
}
|
||||
return try unwrappedResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in observing an async sequence, deferring the fulfilment and results until some condition has been met.
|
||||
/// - Parameters:
|
||||
/// - asyncSequence: The sequence to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - until: callback that evaluates outputs until some condition is reached
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
|
||||
func deferFulfillment<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||
timeout: Duration = .seconds(10),
|
||||
message: String? = nil,
|
||||
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Value> {
|
||||
var result: Result<Value, Error>?
|
||||
var hasFulfilled = false
|
||||
|
||||
let task = Task {
|
||||
for await value in asyncSequence {
|
||||
if condition(value), !hasFulfilled {
|
||||
result = .success(value)
|
||||
hasFulfilled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DeferredFulfillment<Value> {
|
||||
let startTime = ContinuousClock.now
|
||||
|
||||
while !hasFulfilled {
|
||||
await Task.yield()
|
||||
if ContinuousClock.now - startTime >= timeout {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
task.cancel()
|
||||
|
||||
guard let unwrappedResult = result else {
|
||||
throw DeferredFulfillmentError.noOutput(message: message)
|
||||
}
|
||||
return try unwrappedResult.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in subscribing to a publisher and deferring the fulfilment and results until some other actions have been performed.
|
||||
/// - Parameters:
|
||||
/// - publisher: The publisher to wait on.
|
||||
/// - keyPath: the key path for the expected values
|
||||
/// - transitionValues: the values through which the keypath needs to transition through
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
|
||||
func deferFulfillment<P: Publisher, K: KeyPath<P.Output, V>, V: Equatable>(_ publisher: P,
|
||||
keyPath: K,
|
||||
transitionValues: [V],
|
||||
timeout: Duration = .seconds(10)) -> DeferredFulfillment<P.Output> {
|
||||
var expectedOrder = transitionValues
|
||||
return deferFulfillment(publisher, timeout: timeout) { value in
|
||||
let receivedValue = value[keyPath: keyPath]
|
||||
if let index = expectedOrder.firstIndex(where: { $0 == receivedValue }), index == 0 {
|
||||
expectedOrder.remove(at: index)
|
||||
}
|
||||
|
||||
return expectedOrder.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in subscribing to an async sequence and deferring the fulfilment and results until some other actions have been performed.
|
||||
/// - Parameters:
|
||||
/// - asyncSequence: The sequence to wait on.
|
||||
/// - transitionValues: the values through which the sequence needs to transition through
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
|
||||
func deferFulfillment<Value: Equatable>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||
transitionValues: [Value],
|
||||
timeout: Duration = .seconds(10)) -> DeferredFulfillment<Value> {
|
||||
var expectedOrder = transitionValues
|
||||
return deferFulfillment(asyncSequence, timeout: timeout) { value in
|
||||
if let index = expectedOrder.firstIndex(where: { $0 == value }), index == 0 {
|
||||
expectedOrder.remove(at: index)
|
||||
}
|
||||
|
||||
return expectedOrder.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in subscribing to a publisher and deferring the failure for a particular value until some other actions have been performed.
|
||||
/// - Parameters:
|
||||
/// - publisher: The publisher to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - until: callback that evaluates outputs until some condition is reached
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions. The publisher's result is not returned from this fulfilment.
|
||||
func deferFailure<P: Publisher>(_ publisher: P,
|
||||
timeout: Duration,
|
||||
message: String? = nil,
|
||||
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<Void> where P.Failure == Never {
|
||||
var hasFulfilled = false
|
||||
let cancellable = publisher
|
||||
.sink { value in
|
||||
if condition(value), !hasFulfilled {
|
||||
hasFulfilled = true
|
||||
}
|
||||
}
|
||||
|
||||
return DeferredFulfillment<Void> {
|
||||
let startTime = ContinuousClock.now
|
||||
|
||||
while !hasFulfilled {
|
||||
await Task.yield()
|
||||
if ContinuousClock.now - startTime >= timeout {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cancellable.cancel()
|
||||
|
||||
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
|
||||
if hasFulfilled {
|
||||
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility that assists in subscribing to an async sequence and deferring the failure for a particular value until some other actions have been performed.
|
||||
/// - Parameters:
|
||||
/// - asyncSequence: The sequence to wait on.
|
||||
/// - timeout: A timeout after which we give up.
|
||||
/// - until: callback that evaluates outputs until some condition is reached
|
||||
/// - Returns: The deferred fulfilment to be executed after some actions. The sequence's result is not returned from this fulfilment.
|
||||
func deferFailure<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
|
||||
timeout: Duration,
|
||||
message: String? = nil,
|
||||
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Void> {
|
||||
var hasFulfilled = false
|
||||
|
||||
let task = Task {
|
||||
for await value in asyncSequence {
|
||||
if condition(value), !hasFulfilled {
|
||||
hasFulfilled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DeferredFulfillment<Void> {
|
||||
let startTime = ContinuousClock.now
|
||||
|
||||
while !hasFulfilled {
|
||||
await Task.yield()
|
||||
if ContinuousClock.now - startTime >= timeout {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
task.cancel()
|
||||
|
||||
// For deferFailure, if hasFulfilled is true, it means the condition was met (which is a failure)
|
||||
if hasFulfilled {
|
||||
throw DeferredFulfillmentError.unexpectedFulfillment(message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,33 +8,26 @@
|
||||
|
||||
import AVKit
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class AVMetadataMachineReadableCodeObjectExtensionsTest: XCTestCase {
|
||||
func testDecodeQRCodeVersion8() {
|
||||
@Suite
|
||||
struct AVMetadataMachineReadableCodeObjectExtensionsTest {
|
||||
@Test
|
||||
func decodeQRCodeVersion8() throws {
|
||||
// swiftlint:disable:next line_length
|
||||
let rawDataHexString = "4a34d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f0ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec11ec"
|
||||
// swiftlint:disable:next line_length
|
||||
let expectedDecodedString = "4d415452495802048bf94b094096e57d3ea43545604cf59b1704879d295cf7fdd99c62df7866da36005668747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f5f73796e617073652f636c69656e742f72656e64657a766f75732f3031485a32394d345936374a4e315658505759464e355a363638002168747470733a2f2f73796e617073652d6f6964632e656c656d656e742e6465762f"
|
||||
let symbolVersion = 8
|
||||
|
||||
guard let data = Data(hexString: rawDataHexString) else {
|
||||
XCTFail("Could not initialise the raw data")
|
||||
return
|
||||
}
|
||||
let data = try #require(Data(hexString: rawDataHexString))
|
||||
|
||||
guard let resultData = try? AVMetadataMachineReadableCodeObject.removeQRProtocolData(data, symbolVersion: symbolVersion) else {
|
||||
XCTFail("Could not remove the protocol data")
|
||||
return
|
||||
}
|
||||
let resultData = try #require(try AVMetadataMachineReadableCodeObject.removeQRProtocolData(data, symbolVersion: symbolVersion))
|
||||
|
||||
let resultString = resultData.map { String(format: "%02x", $0) }.joined()
|
||||
XCTAssertEqual(expectedDecodedString, resultString)
|
||||
#expect(expectedDecodedString == resultString)
|
||||
|
||||
guard let expectedResultData = Data(hexString: expectedDecodedString) else {
|
||||
XCTFail("Could not initialise the decoded data")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(expectedResultData, resultData)
|
||||
let expectedResultData = try #require(Data(hexString: expectedDecodedString))
|
||||
#expect(expectedResultData == resultData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,16 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AnalyticsSettingsScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class AnalyticsSettingsScreenViewModelTests {
|
||||
private var appSettings: AppSettings!
|
||||
private var viewModel: AnalyticsSettingsScreenViewModelProtocol!
|
||||
private var context: AnalyticsSettingsScreenViewModelType.Context!
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
let analyticsClient = AnalyticsClientMock()
|
||||
@@ -35,20 +28,27 @@ class AnalyticsSettingsScreenViewModelTests: XCTestCase {
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(context.enableAnalytics)
|
||||
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testOptIn() {
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(!context.enableAnalytics)
|
||||
}
|
||||
|
||||
@Test
|
||||
func optIn() {
|
||||
appSettings.analyticsConsentState = .optedOut
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
XCTAssertTrue(context.enableAnalytics)
|
||||
#expect(context.enableAnalytics)
|
||||
}
|
||||
|
||||
func testOptOut() {
|
||||
@Test
|
||||
func optOut() {
|
||||
appSettings.analyticsConsentState = .optedIn
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
XCTAssertFalse(context.enableAnalytics)
|
||||
#expect(!context.enableAnalytics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
import AnalyticsEvents
|
||||
@testable import ElementX
|
||||
import PostHog
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class AnalyticsTests: XCTestCase {
|
||||
private var appSettings: AppSettings!
|
||||
private var analyticsClient: AnalyticsClientMock!
|
||||
private var posthogMock: PHGPostHogMock!
|
||||
@Suite
|
||||
final class AnalyticsTests {
|
||||
private var appSettings: AppSettings
|
||||
private var analyticsClient: AnalyticsClientMock
|
||||
private var posthogMock: PHGPostHogMock
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
|
||||
@@ -29,20 +30,22 @@ class AnalyticsTests: XCTestCase {
|
||||
posthogMock.configureMockBehavior()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testAnalyticsPromptNewUser() {
|
||||
@Test
|
||||
func analyticsPromptNewUser() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When the user is prompted for analytics.
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then the prompt should be shown.
|
||||
XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
|
||||
#expect(showPrompt, "A prompt should be shown for a new user.")
|
||||
}
|
||||
|
||||
func testAnalyticsPromptUserDeclinedPostHog() {
|
||||
@Test
|
||||
func analyticsPromptUserDeclinedPostHog() {
|
||||
// Given an existing install of the app where the user previously declined PostHog
|
||||
appSettings.analyticsConsentState = .optedOut
|
||||
|
||||
@@ -50,10 +53,11 @@ class AnalyticsTests: XCTestCase {
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then no prompt should be shown.
|
||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||
#expect(!showPrompt, "A prompt should not be shown any more.")
|
||||
}
|
||||
|
||||
func testAnalyticsPromptUserAcceptedPostHog() {
|
||||
@Test
|
||||
func analyticsPromptUserAcceptedPostHog() {
|
||||
// Given an existing install of the app where the user previously accepted PostHog
|
||||
appSettings.analyticsConsentState = .optedIn
|
||||
|
||||
@@ -61,61 +65,67 @@ class AnalyticsTests: XCTestCase {
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then no prompt should be shown.
|
||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||
#expect(!showPrompt, "A prompt should not be shown any more.")
|
||||
}
|
||||
|
||||
func testAnalyticsPromptNotDisplayed() {
|
||||
@Test
|
||||
func analyticsPromptNotDisplayed() {
|
||||
// Given a fresh install of the app Analytics should be disabled
|
||||
XCTAssertEqual(appSettings.analyticsConsentState, .unknown)
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
|
||||
XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
#expect(appSettings.analyticsConsentState == .unknown)
|
||||
#expect(!ServiceLocator.shared.analytics.isEnabled)
|
||||
#expect(!analyticsClient.startAnalyticsConfigurationCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsOptOut() {
|
||||
@Test
|
||||
func analyticsOptOut() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When analytics is opt-out
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
// Then analytics should be disabled
|
||||
XCTAssertEqual(appSettings.analyticsConsentState, .optedOut)
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
|
||||
XCTAssertFalse(analyticsClient.isRunning)
|
||||
#expect(appSettings.analyticsConsentState == .optedOut)
|
||||
#expect(!ServiceLocator.shared.analytics.isEnabled)
|
||||
#expect(!analyticsClient.isRunning)
|
||||
// Analytics client should have been stopped
|
||||
XCTAssertTrue(analyticsClient.stopCalled)
|
||||
#expect(analyticsClient.stopCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsOptIn() {
|
||||
@Test
|
||||
func analyticsOptIn() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When analytics is opt-in
|
||||
ServiceLocator.shared.analytics.optIn()
|
||||
// The analytics should be enabled
|
||||
XCTAssertEqual(appSettings.analyticsConsentState, .optedIn)
|
||||
XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled)
|
||||
#expect(appSettings.analyticsConsentState == .optedIn)
|
||||
#expect(ServiceLocator.shared.analytics.isEnabled)
|
||||
// Analytics client should have been started
|
||||
XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
#expect(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsStartIfNotEnabled() {
|
||||
@Test
|
||||
func analyticsStartIfNotEnabled() {
|
||||
// Given an existing install of the app where the user previously declined the tracking
|
||||
appSettings.analyticsConsentState = .optedOut
|
||||
// Analytics should not start
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.isEnabled)
|
||||
#expect(!ServiceLocator.shared.analytics.isEnabled)
|
||||
ServiceLocator.shared.analytics.startIfEnabled()
|
||||
XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
#expect(!analyticsClient.startAnalyticsConfigurationCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsStartIfEnabled() {
|
||||
@Test
|
||||
func analyticsStartIfEnabled() {
|
||||
// Given an existing install of the app where the user previously accepted the tracking
|
||||
appSettings.analyticsConsentState = .optedIn
|
||||
// Analytics should start
|
||||
XCTAssertTrue(ServiceLocator.shared.analytics.isEnabled)
|
||||
#expect(ServiceLocator.shared.analytics.isEnabled)
|
||||
ServiceLocator.shared.analytics.startIfEnabled()
|
||||
XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
#expect(analyticsClient.startAnalyticsConfigurationCalled)
|
||||
}
|
||||
|
||||
func testAddingUserProperties() {
|
||||
@Test
|
||||
func addingUserProperties() {
|
||||
// Given a client with no user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")
|
||||
#expect(client.pendingUserProperties == nil, "No user properties should have been set yet.")
|
||||
|
||||
// When updating the user properties
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil,
|
||||
@@ -124,25 +134,26 @@ class AnalyticsTests: XCTestCase {
|
||||
numSpaces: 5, recoveryState: .Disabled, verificationState: .Verified))
|
||||
|
||||
// Then the properties should be cached
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should match.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.verificationState, AnalyticsEvent.UserProperties.VerificationState.Verified, "The verification state should match.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.recoveryState, AnalyticsEvent.UserProperties.RecoveryState.Disabled, "The recovery state should match.")
|
||||
#expect(client.pendingUserProperties != nil, "The user properties should be cached.")
|
||||
#expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection should match.")
|
||||
#expect(client.pendingUserProperties?.numFavouriteRooms == 4, "The number of favorite rooms should match.")
|
||||
#expect(client.pendingUserProperties?.numSpaces == 5, "The number of spaces should match.")
|
||||
#expect(client.pendingUserProperties?.verificationState == AnalyticsEvent.UserProperties.VerificationState.Verified, "The verification state should match.")
|
||||
#expect(client.pendingUserProperties?.recoveryState == AnalyticsEvent.UserProperties.RecoveryState.Disabled, "The recovery state should match.")
|
||||
}
|
||||
|
||||
func testMergingUserProperties() {
|
||||
@Test
|
||||
func mergingUserProperties() {
|
||||
// Given a client with a cached use case user properties
|
||||
let client = PostHogAnalyticsClient()
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
|
||||
numFavouriteRooms: nil,
|
||||
numSpaces: nil, recoveryState: nil, verificationState: nil))
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.")
|
||||
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")
|
||||
#expect(client.pendingUserProperties != nil, "The user properties should be cached.")
|
||||
#expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection should match.")
|
||||
#expect(client.pendingUserProperties?.numFavouriteRooms == nil, "The number of favorite rooms should not be set.")
|
||||
#expect(client.pendingUserProperties?.numSpaces == nil, "The number of spaces should not be set.")
|
||||
|
||||
// When updating the number of spaced
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil,
|
||||
@@ -150,24 +161,25 @@ class AnalyticsTests: XCTestCase {
|
||||
numSpaces: 5, recoveryState: nil, verificationState: nil))
|
||||
|
||||
// Then the new properties should be updated and the existing properties should remain unchanged
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numFavouriteRooms, 4, "The number of favorite rooms should have been updated.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
|
||||
#expect(client.pendingUserProperties != nil, "The user properties should be cached.")
|
||||
#expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection shouldn't have changed.")
|
||||
#expect(client.pendingUserProperties?.numFavouriteRooms == 4, "The number of favorite rooms should have been updated.")
|
||||
#expect(client.pendingUserProperties?.numSpaces == 5, "The number of spaces should have been updated.")
|
||||
}
|
||||
|
||||
func testSendingUserProperties() throws {
|
||||
@Test
|
||||
func sendingUserProperties() throws {
|
||||
// Given a client with user properties set
|
||||
|
||||
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
|
||||
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
|
||||
try client.start(analyticsConfiguration: #require(appSettings.analyticsConfiguration))
|
||||
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging,
|
||||
numFavouriteRooms: nil,
|
||||
numSpaces: nil, recoveryState: nil, verificationState: nil))
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
#expect(client.pendingUserProperties != nil, "The user properties should be cached.")
|
||||
#expect(client.pendingUserProperties?.ftueUseCaseSelection == .PersonalMessaging, "The use case selection should match.")
|
||||
|
||||
// When sending an event (tests run under Debug configuration so this is sent to the development instance)
|
||||
let someEvent = AnalyticsEvent.Error(context: nil,
|
||||
@@ -186,29 +198,31 @@ class AnalyticsTests: XCTestCase {
|
||||
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
|
||||
|
||||
// The user properties should have been added
|
||||
XCTAssertEqual(capturedEvent?.userProperties?["ftueUseCaseSelection"] as? String, AnalyticsEvent.UserProperties.FtueUseCaseSelection.PersonalMessaging.rawValue)
|
||||
#expect(capturedEvent?.userProperties?["ftueUseCaseSelection"] as? String == AnalyticsEvent.UserProperties.FtueUseCaseSelection.PersonalMessaging.rawValue)
|
||||
|
||||
// Then the properties should be cleared
|
||||
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||
#expect(client.pendingUserProperties == nil, "The user properties should be cleared.")
|
||||
}
|
||||
|
||||
func testResetConsentState() {
|
||||
@Test
|
||||
func resetConsentState() {
|
||||
// Given an existing install of the app where the user previously accpeted the tracking
|
||||
appSettings.analyticsConsentState = .optedIn
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
|
||||
#expect(!ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
|
||||
|
||||
// When forgetting analytics consents
|
||||
ServiceLocator.shared.analytics.resetConsentState()
|
||||
|
||||
// Then the analytics prompt should be presented again
|
||||
XCTAssertEqual(appSettings.analyticsConsentState, .unknown)
|
||||
XCTAssertTrue(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
|
||||
#expect(appSettings.analyticsConsentState == .unknown)
|
||||
#expect(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt)
|
||||
}
|
||||
|
||||
func testSendingAndUpdatingSuperProperties() throws {
|
||||
@Test
|
||||
func sendingAndUpdatingSuperProperties() throws {
|
||||
// Given a client with user properties set
|
||||
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
|
||||
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
|
||||
try client.start(analyticsConfiguration: #require(appSettings.analyticsConfiguration))
|
||||
|
||||
client.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI,
|
||||
cryptoSDK: .Rust,
|
||||
@@ -219,12 +233,12 @@ class AnalyticsTests: XCTestCase {
|
||||
|
||||
let screenEvent = posthogMock.screenPropertiesReceivedArguments
|
||||
|
||||
XCTAssertEqual(screenEvent?.screenTitle, AnalyticsEvent.MobileScreen.ScreenName.Home.rawValue)
|
||||
#expect(screenEvent?.screenTitle == AnalyticsEvent.MobileScreen.ScreenName.Home.rawValue)
|
||||
|
||||
// All the super properties should have been added
|
||||
XCTAssertEqual(screenEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
XCTAssertEqual(screenEvent?.properties?["appPlatform"] as? String, "EXI")
|
||||
XCTAssertEqual(screenEvent?.properties?["cryptoSDKVersion"] as? String, "000")
|
||||
#expect(screenEvent?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
#expect(screenEvent?.properties?["appPlatform"] as? String == "EXI")
|
||||
#expect(screenEvent?.properties?["cryptoSDKVersion"] as? String == "000")
|
||||
|
||||
// It should be the same for any event
|
||||
let someEvent = AnalyticsEvent.Error(context: nil,
|
||||
@@ -243,9 +257,9 @@ class AnalyticsTests: XCTestCase {
|
||||
let capturedEvent = posthogMock.capturePropertiesUserPropertiesReceivedArguments
|
||||
|
||||
// All the super properties should have been added
|
||||
XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
XCTAssertEqual(capturedEvent?.properties?["appPlatform"] as? String, "EXI")
|
||||
XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "000")
|
||||
#expect(capturedEvent?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
#expect(capturedEvent?.properties?["appPlatform"] as? String == "EXI")
|
||||
#expect(capturedEvent?.properties?["cryptoSDKVersion"] as? String == "000")
|
||||
|
||||
// Updating should keep the previously set properties
|
||||
client.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI,
|
||||
@@ -256,20 +270,21 @@ class AnalyticsTests: XCTestCase {
|
||||
let capturedEvent2 = posthogMock.capturePropertiesUserPropertiesReceivedArguments
|
||||
|
||||
// All the super properties should have been added, with the one udpated
|
||||
XCTAssertEqual(capturedEvent2?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
XCTAssertEqual(capturedEvent2?.properties?["appPlatform"] as? String, "EXI")
|
||||
XCTAssertEqual(capturedEvent2?.properties?["cryptoSDKVersion"] as? String, "001")
|
||||
#expect(capturedEvent2?.properties?["cryptoSDK"] as? String == AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue)
|
||||
#expect(capturedEvent2?.properties?["appPlatform"] as? String == "EXI")
|
||||
#expect(capturedEvent2?.properties?["cryptoSDKVersion"] as? String == "001")
|
||||
}
|
||||
|
||||
func testShouldNotReportIfNotStarted() throws {
|
||||
@Test
|
||||
func shouldNotReportIfNotStarted() throws {
|
||||
// Given a client with user properties set
|
||||
let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock))
|
||||
|
||||
|
||||
// No call to start
|
||||
|
||||
client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home))
|
||||
|
||||
XCTAssertEqual(posthogMock.screenPropertiesCalled, false)
|
||||
#expect(posthogMock.screenPropertiesCalled == false)
|
||||
|
||||
// It should be the same for any event
|
||||
let someEvent = AnalyticsEvent.Error(context: nil,
|
||||
@@ -285,13 +300,13 @@ class AnalyticsTests: XCTestCase {
|
||||
wasVisibleToUser: nil)
|
||||
client.capture(someEvent)
|
||||
|
||||
XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, false)
|
||||
#expect(posthogMock.capturePropertiesUserPropertiesCalled == false)
|
||||
|
||||
// start now
|
||||
try client.start(analyticsConfiguration: XCTUnwrap(appSettings.analyticsConfiguration))
|
||||
XCTAssertEqual(posthogMock.optInCalled, true)
|
||||
try client.start(analyticsConfiguration: #require(appSettings.analyticsConfiguration))
|
||||
#expect(posthogMock.optInCalled == true)
|
||||
|
||||
client.capture(someEvent)
|
||||
XCTAssertEqual(posthogMock.capturePropertiesUserPropertiesCalled, true)
|
||||
#expect(posthogMock.capturePropertiesUserPropertiesCalled == true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,20 +7,21 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AppLockScreenViewModelTests: XCTestCase {
|
||||
var appSettings: AppSettings!
|
||||
var appLockService: AppLockService!
|
||||
var keychainController: KeychainControllerMock!
|
||||
var viewModel: AppLockScreenViewModelProtocol!
|
||||
@Suite
|
||||
final class AppLockScreenViewModelTests {
|
||||
var appSettings: AppSettings
|
||||
var appLockService: AppLockService
|
||||
var keychainController: KeychainControllerMock
|
||||
var viewModel: AppLockScreenViewModelProtocol
|
||||
|
||||
var context: AppLockScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
keychainController = KeychainControllerMock()
|
||||
@@ -28,11 +29,12 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
viewModel = AppLockScreenViewModel(appLockService: appLockService)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testUnlock() async throws {
|
||||
@Test
|
||||
func unlock() async throws {
|
||||
// Given a valid PIN code.
|
||||
let pinCode = "2023"
|
||||
keychainController.pinCodeReturnValue = pinCode
|
||||
@@ -44,18 +46,19 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
let result = try await deferred.fulfill()
|
||||
|
||||
// The app should become unlocked.
|
||||
XCTAssertEqual(result, .appUnlocked)
|
||||
#expect(result == .appUnlocked)
|
||||
}
|
||||
|
||||
func testForgotPIN() async throws {
|
||||
@Test
|
||||
func forgotPIN() async throws {
|
||||
// Given a fresh launch of the app.
|
||||
XCTAssertNil(context.alertInfo, "No alert should be shown initially.")
|
||||
#expect(context.alertInfo == nil, "No alert should be shown initially.")
|
||||
|
||||
// When the user has forgotten their PIN.
|
||||
context.send(viewAction: .forgotPIN)
|
||||
|
||||
// Then an alert should be shown before logging out.
|
||||
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "An alert should be shown before logging out.")
|
||||
#expect(context.alertInfo?.id == .confirmResetPIN, "An alert should be shown before logging out.")
|
||||
|
||||
// When confirming the logout.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
|
||||
@@ -65,14 +68,15 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUnlockFailure() async throws {
|
||||
@Test
|
||||
func unlockFailure() async throws {
|
||||
// Given an invalid PIN code.
|
||||
let pinCode = "2024"
|
||||
keychainController.pinCodeReturnValue = "2023"
|
||||
keychainController.containsPINCodeBiometricStateReturnValue = false
|
||||
XCTAssertEqual(context.viewState.numberOfPINAttempts, 0, "The shouldn't be any attempts yet.")
|
||||
XCTAssertFalse(context.viewState.isSubtitleWarning, "No warning should be shown yet.")
|
||||
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
|
||||
#expect(context.viewState.numberOfPINAttempts == 0, "The shouldn't be any attempts yet.")
|
||||
#expect(!context.viewState.isSubtitleWarning, "No warning should be shown yet.")
|
||||
#expect(context.alertInfo == nil, "No alert should be shown yet.")
|
||||
|
||||
// When entering it on the lock screen.
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 1 }
|
||||
@@ -81,9 +85,9 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .clearPINCode) // Simulate the animation completion
|
||||
|
||||
// Then a failed attempt should be shown.
|
||||
XCTAssertEqual(context.viewState.numberOfPINAttempts, 1, "A failed attempt should have been recorded.")
|
||||
XCTAssertTrue(context.viewState.isSubtitleWarning, "A warning should now be shown.")
|
||||
XCTAssertNil(context.alertInfo, "No alert should be shown yet.")
|
||||
#expect(context.viewState.numberOfPINAttempts == 1, "A failed attempt should have been recorded.")
|
||||
#expect(context.viewState.isSubtitleWarning, "A warning should now be shown.")
|
||||
#expect(context.alertInfo == nil, "No alert should be shown yet.")
|
||||
|
||||
// When entering twice more
|
||||
deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 2 }
|
||||
@@ -96,28 +100,28 @@ class AppLockScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .clearPINCode) // Simulate the animation completion
|
||||
|
||||
// Then an alert should be shown
|
||||
XCTAssertEqual(context.viewState.numberOfPINAttempts, 3, "All the attempts should have been recorded.")
|
||||
XCTAssertTrue(context.viewState.isSubtitleWarning, "The warning should still be shown.")
|
||||
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "An alert should now be shown.")
|
||||
#expect(context.viewState.numberOfPINAttempts == 3, "All the attempts should have been recorded.")
|
||||
#expect(context.viewState.isSubtitleWarning, "The warning should still be shown.")
|
||||
#expect(context.alertInfo?.id == .forcedLogout, "An alert should now be shown.")
|
||||
}
|
||||
|
||||
func testForceQuitRequiresLogout() async throws {
|
||||
@Test
|
||||
func forceQuitRequiresLogout() async throws {
|
||||
// Given an app with a PIN set where the user attempted to unlock 3 times.
|
||||
keychainController.pinCodeReturnValue = "2023"
|
||||
keychainController.containsPINCodeBiometricStateReturnValue = false
|
||||
appSettings.appLockNumberOfPINAttempts = 2
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.alertInfo == nil)
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.numberOfPINAttempts == 3 }
|
||||
viewModel.context.pinCode = "0000"
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 3, "The app should have 3 failed attempts before the force quit.")
|
||||
XCTAssertEqual(context.alertInfo?.id, .forcedLogout, "The app should be showing the alert before the force quit.")
|
||||
#expect(appSettings.appLockNumberOfPINAttempts == 3, "The app should have 3 failed attempts before the force quit.")
|
||||
#expect(context.alertInfo?.id == .forcedLogout, "The app should be showing the alert before the force quit.")
|
||||
|
||||
// When force quitting the app and relaunching.
|
||||
viewModel = nil
|
||||
let freshViewModel = AppLockScreenViewModel(appLockService: appLockService)
|
||||
|
||||
// Then the alert should remain in place
|
||||
XCTAssertEqual(freshViewModel.context.alertInfo?.id, .forcedLogout, "The new view model from the fresh launch should also show the alert")
|
||||
#expect(freshViewModel.context.alertInfo?.id == .forcedLogout, "The new view model from the fresh launch should also show the alert")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AppLockServiceTests: XCTestCase {
|
||||
var keychainController: KeychainController!
|
||||
var appSettings: AppSettings!
|
||||
var service: AppLockService!
|
||||
@Suite
|
||||
final class AppLockServiceTests {
|
||||
private var keychainController: KeychainController
|
||||
private var appSettings: AppSettings
|
||||
private var service: AppLockService
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
|
||||
@@ -26,34 +28,36 @@ class AppLockServiceTests: XCTestCase {
|
||||
service.disable()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
// MARK: - PIN Code
|
||||
|
||||
func testValidPINCode() {
|
||||
@Test
|
||||
func validPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
|
||||
// When setting a PIN code.
|
||||
let pinCode = "2023" // Highly secure PIN that is rotated every 12 months.
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
|
||||
// Then service should be enabled and only the provided PIN should work to unlock the app.
|
||||
XCTAssertTrue(service.isEnabled, "The service should become enabled when setting a PIN.")
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The provided PIN code should work.")
|
||||
XCTAssertFalse(service.unlock(with: "2024"), "No other PIN code should work.")
|
||||
XCTAssertFalse(service.unlock(with: "1234"), "No other PIN code should work.")
|
||||
XCTAssertFalse(service.unlock(with: "9999"), "No other PIN code should work.")
|
||||
#expect(service.isEnabled, "The service should become enabled when setting a PIN.")
|
||||
#expect(service.unlock(with: pinCode), "The provided PIN code should work.")
|
||||
#expect(!service.unlock(with: "2024"), "No other PIN code should work.")
|
||||
#expect(!service.unlock(with: "1234"), "No other PIN code should work.")
|
||||
#expect(!service.unlock(with: "9999"), "No other PIN code should work.")
|
||||
}
|
||||
|
||||
func testWeakPINCode() {
|
||||
@Test
|
||||
func weakPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
|
||||
// When setting a PIN code that is in the block list.
|
||||
let pinCode = appSettings.appLockPINCodeBlockList[0]
|
||||
@@ -61,16 +65,17 @@ class AppLockServiceTests: XCTestCase {
|
||||
|
||||
// Then the setup should fail and the service be left as disabled.
|
||||
guard case let .failure(error) = result else {
|
||||
XCTFail("The call should have failed.")
|
||||
Issue.record("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
#expect(error == .weakPIN, "The PIN should be rejected as weak.")
|
||||
#expect(!service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testShortPINCode() {
|
||||
@Test
|
||||
func shortPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
|
||||
// When setting a PIN code that is too short
|
||||
let pinCode = "123"
|
||||
@@ -78,16 +83,17 @@ class AppLockServiceTests: XCTestCase {
|
||||
|
||||
// Then the setup should fail and the service be left as disabled.
|
||||
guard case let .failure(error) = result else {
|
||||
XCTFail("The call should have failed.")
|
||||
Issue.record("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
#expect(!service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testNonNumericPINCode() {
|
||||
@Test
|
||||
func nonNumericPINCode() {
|
||||
// Given a service that hasn't been enabled.
|
||||
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
#expect(!service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||
|
||||
// When setting a PIN code that is too short
|
||||
let pinCode = "abcd"
|
||||
@@ -95,116 +101,121 @@ class AppLockServiceTests: XCTestCase {
|
||||
|
||||
// Then the setup should fail and the service be left as disabled.
|
||||
guard case let .failure(error) = result else {
|
||||
XCTFail("The call should have failed.")
|
||||
Issue.record("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||
#expect(error == .invalidPIN, "The PIN should be rejected as invalid.")
|
||||
#expect(!service.isEnabled, "The service should remain disabled.")
|
||||
}
|
||||
|
||||
func testChangePINCode() {
|
||||
@Test
|
||||
func changePINCode() {
|
||||
// Given a service that is already enabled with a PIN.
|
||||
let pinCode = "2023"
|
||||
let newPINCode = "2024"
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
XCTAssertFalse(service.unlock(with: newPINCode), "The PIN we're about to set should not work.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
#expect(!service.unlock(with: newPINCode), "The PIN we're about to set should not work.")
|
||||
|
||||
// When updating the PIN code.
|
||||
guard case .success = service.setupPINCode(newPINCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
|
||||
// Then the old code should not be accepted.
|
||||
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertTrue(service.unlock(with: newPINCode), "The new PIN should work.")
|
||||
XCTAssertFalse(service.unlock(with: pinCode), "The original PIN should be rejected.")
|
||||
#expect(service.isEnabled, "The service should remain enabled.")
|
||||
#expect(service.unlock(with: newPINCode), "The new PIN should work.")
|
||||
#expect(!service.unlock(with: pinCode), "The original PIN should be rejected.")
|
||||
}
|
||||
|
||||
func testInvalidChangePINCode() {
|
||||
@Test
|
||||
func invalidChangePINCode() {
|
||||
// Given a service that is already enabled with a PIN.
|
||||
let pinCode = "2023"
|
||||
let invalidPIN = appSettings.appLockPINCodeBlockList[0]
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
XCTAssertFalse(service.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
#expect(!service.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
|
||||
|
||||
// When updating the PIN code that is in the block list.
|
||||
let result = service.setupPINCode(invalidPIN)
|
||||
|
||||
// Then it should fail and nothing should change.
|
||||
guard case let .failure(error) = result else {
|
||||
XCTFail("The call should have failed.")
|
||||
Issue.record("The call should have failed.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
|
||||
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertFalse(service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The original PIN should continue to work.")
|
||||
#expect(error == .weakPIN, "The PIN should be rejected as weak.")
|
||||
#expect(service.isEnabled, "The service should remain enabled.")
|
||||
#expect(!service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
|
||||
#expect(service.unlock(with: pinCode), "The original PIN should continue to work.")
|
||||
}
|
||||
|
||||
func testDisablePINCode() {
|
||||
@Test
|
||||
func disablePINCode() {
|
||||
// Given a service that is already enabled with a PIN.
|
||||
let pinCode = "2023"
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||
|
||||
// When disabling the PIN code.
|
||||
service.disable()
|
||||
|
||||
// Then the PIN code should be removed.
|
||||
XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.")
|
||||
XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
|
||||
#expect(!service.isEnabled, "The service should no longer be enabled.")
|
||||
#expect(!service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
|
||||
}
|
||||
|
||||
// MARK: - Biometric Unlock
|
||||
|
||||
func testEnableBiometricUnlock() async {
|
||||
@Test
|
||||
func enableBiometricUnlock() async {
|
||||
// Given a service with the PIN code already set.
|
||||
let context = LAContextMock()
|
||||
context.biometryTypeValue = .touchID
|
||||
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
|
||||
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
|
||||
guard case .success = service.setupPINCode("2023") else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
|
||||
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should not be enabled.")
|
||||
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should not be trusted.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
|
||||
#expect(!service.biometricUnlockEnabled, "Biometric unlock should not be enabled.")
|
||||
#expect(!service.biometricUnlockTrusted, "Biometric unlock should not be trusted.")
|
||||
|
||||
// When enabling biometric unlock.
|
||||
guard case .success = service.enableBiometricUnlock() else {
|
||||
XCTFail("The biometric lock should enable.")
|
||||
Issue.record("The biometric lock should enable.")
|
||||
return
|
||||
}
|
||||
context.evaluatePolicyReturnValue = true
|
||||
|
||||
// Then the service should be unlockable with biometrics.
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should not change.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should now be enabled.")
|
||||
#expect(service.biometricUnlockTrusted, "Biometric unlock should now be trusted.")
|
||||
guard await service.unlockWithBiometrics() == .unlocked else {
|
||||
XCTFail("The biometric unlock should work.")
|
||||
Issue.record("The biometric unlock should work.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testBiometricUnlockTrust() {
|
||||
@Test
|
||||
func biometricUnlockTrust() {
|
||||
// Given a service with the PIN code already set.
|
||||
let context = LAContextMock()
|
||||
context.biometryTypeValue = .touchID
|
||||
@@ -212,129 +223,133 @@ class AppLockServiceTests: XCTestCase {
|
||||
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
|
||||
let pinCode = "2023"
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
guard case .success = service.enableBiometricUnlock() else {
|
||||
XCTFail("The biometric lock should enable.")
|
||||
Issue.record("The biometric lock should enable.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
|
||||
// When the user changes biometric data.
|
||||
context.evaluatedPolicyDomainStateValue = Data("👈".utf8)
|
||||
|
||||
// Then biometric lock should remain enabled but untrusted.
|
||||
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
|
||||
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
#expect(service.isEnabled, "The service should remain enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should not change.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
|
||||
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
|
||||
// When the user confirms their PIN code.
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The PIN code should be accepted")
|
||||
#expect(service.unlock(with: pinCode), "The PIN code should be accepted")
|
||||
|
||||
// Then the biometric lock should once again be trusted.
|
||||
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.")
|
||||
#expect(service.isEnabled, "The service should remain enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should not change.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should remain enabled.")
|
||||
#expect(service.biometricUnlockTrusted, "Biometric unlock should once again be trusted.")
|
||||
}
|
||||
|
||||
func testDisableBiometricUnlock() {
|
||||
@Test
|
||||
func disableBiometricUnlock() {
|
||||
// Given a service with the PIN code already set.
|
||||
let context = LAContextMock()
|
||||
context.biometryTypeValue = .touchID
|
||||
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
|
||||
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
|
||||
guard case .success = service.setupPINCode("2023") else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
guard case .success = service.enableBiometricUnlock() else {
|
||||
XCTFail("The biometric lock should enable.")
|
||||
Issue.record("The biometric lock should enable.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should be in sync with the mock.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should be in sync with the mock.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
|
||||
// When disabling biometric unlock.
|
||||
service.disableBiometricUnlock()
|
||||
|
||||
// Then only PIN unlock should remain enabled.
|
||||
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertEqual(service.biometryType, .touchID, "The biometry type should not change.")
|
||||
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
|
||||
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
#expect(service.isEnabled, "The service should remain enabled.")
|
||||
#expect(service.biometryType == .touchID, "The biometry type should not change.")
|
||||
#expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
|
||||
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
}
|
||||
|
||||
func testDisablePINWithBiometricUnlock() {
|
||||
@Test
|
||||
func disablePINWithBiometricUnlock() {
|
||||
// Given a service with the PIN code already set.
|
||||
let context = LAContextMock()
|
||||
context.biometryTypeValue = .touchID
|
||||
context.evaluatedPolicyDomainStateValue = Data("👆".utf8)
|
||||
service = AppLockService(keychainController: keychainController, appSettings: appSettings, context: context)
|
||||
guard case .success = service.setupPINCode("2023") else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
guard case .success = service.enableBiometricUnlock() else {
|
||||
XCTFail("The biometric lock should enable.")
|
||||
Issue.record("The biometric lock should enable.")
|
||||
return
|
||||
}
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
XCTAssertTrue(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
#expect(service.biometricUnlockEnabled, "Biometric unlock should be enabled.")
|
||||
#expect(service.biometricUnlockTrusted, "Biometric unlock should be trusted.")
|
||||
|
||||
// When disabling the PIN lock.
|
||||
service.disable()
|
||||
|
||||
// Then both PIN and biometric unlock should be disabled.
|
||||
XCTAssertFalse(service.isEnabled, "The service should remain enabled.")
|
||||
XCTAssertFalse(service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
|
||||
XCTAssertFalse(service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
#expect(!service.isEnabled, "The service should remain enabled.")
|
||||
#expect(!service.biometricUnlockEnabled, "Biometric unlock should become disabled.")
|
||||
#expect(!service.biometricUnlockTrusted, "Biometric unlock should no longer be trusted.")
|
||||
}
|
||||
|
||||
// MARK: - Attempt failures
|
||||
|
||||
func testResetAttemptsOnUnlock() {
|
||||
@Test
|
||||
func resetAttemptsOnUnlock() {
|
||||
// Given a service that is enabled and has failed unlock attempts.
|
||||
let pinCode = "2023"
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
appSettings.appLockNumberOfPINAttempts = 2
|
||||
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
|
||||
// When unlocking the service
|
||||
XCTAssertTrue(service.unlock(with: pinCode), "The PIN should work.")
|
||||
#expect(service.unlock(with: pinCode), "The PIN should work.")
|
||||
|
||||
// Then the attempts counts should both be reset.
|
||||
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
|
||||
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
|
||||
}
|
||||
|
||||
func testResetAttemptsOnDisable() {
|
||||
@Test
|
||||
func resetAttemptsOnDisable() {
|
||||
// Given a service that is enabled and has failed unlock attempts.
|
||||
let pinCode = "2023"
|
||||
guard case .success = service.setupPINCode(pinCode) else {
|
||||
XCTFail("The PIN should be valid.")
|
||||
Issue.record("The PIN should be valid.")
|
||||
return
|
||||
}
|
||||
appSettings.appLockNumberOfPINAttempts = 2
|
||||
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 2, "The initial conditions should be stored.")
|
||||
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||
#expect(appSettings.appLockNumberOfPINAttempts == 2, "The initial conditions should be stored.")
|
||||
#expect(service.isEnabled, "The service should be enabled.")
|
||||
|
||||
// When disabling the service
|
||||
service.disable()
|
||||
XCTAssertFalse(service.isEnabled, "The service should be disabled.")
|
||||
#expect(!service.isEnabled, "The service should be disabled.")
|
||||
|
||||
// Then the attempts counts should both be reset.
|
||||
XCTAssertEqual(appSettings.appLockNumberOfPINAttempts, 0, "The PIN attempts should be reset.")
|
||||
#expect(appSettings.appLockNumberOfPINAttempts == 0, "The PIN attempts should be reset.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,39 +7,40 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AppLockSetupSettingsScreenViewModelTests: XCTestCase {
|
||||
var appLockService: AppLockServiceProtocol!
|
||||
var keychainController: KeychainControllerMock!
|
||||
var viewModel: AppLockSetupSettingsScreenViewModelProtocol!
|
||||
@Suite
|
||||
struct AppLockSetupSettingsScreenViewModelTests {
|
||||
var appLockService: AppLockServiceProtocol
|
||||
var keychainController: KeychainControllerMock
|
||||
var viewModel: AppLockSetupSettingsScreenViewModelProtocol
|
||||
|
||||
var context: AppLockSetupSettingsScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
init() {
|
||||
keychainController = KeychainControllerMock()
|
||||
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
|
||||
|
||||
viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock())
|
||||
}
|
||||
|
||||
func testDisablingShowsAlert() {
|
||||
@Test
|
||||
func disablingShowsAlert() {
|
||||
// Given a fresh screen with the PIN code enabled.
|
||||
let pinCode = "2023"
|
||||
keychainController.pinCodeReturnValue = pinCode
|
||||
keychainController.containsPINCodeReturnValue = true
|
||||
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(appLockService.isEnabled)
|
||||
#expect(context.alertInfo == nil)
|
||||
#expect(appLockService.isEnabled)
|
||||
|
||||
// When disabling the PIN code lock.
|
||||
context.send(viewAction: .disable)
|
||||
|
||||
// Then an alert should be shown before disabling it.
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
XCTAssertTrue(appLockService.isEnabled)
|
||||
#expect(context.alertInfo != nil)
|
||||
#expect(appLockService.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AppLockSetupBiometricsScreenViewModelTests: XCTestCase {
|
||||
var appLockService: AppLockServiceMock!
|
||||
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol!
|
||||
@Suite
|
||||
final class AppLockSetupBiometricsScreenViewModelTests {
|
||||
var appLockService: AppLockServiceMock
|
||||
var viewModel: AppLockSetupBiometricsScreenViewModelProtocol
|
||||
|
||||
var context: AppLockSetupBiometricsScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
|
||||
appLockService = AppLockServiceMock()
|
||||
@@ -28,27 +29,29 @@ class AppLockSetupBiometricsScreenViewModelTests: XCTestCase {
|
||||
viewModel = AppLockSetupBiometricsScreenViewModel(appLockService: appLockService)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testAllow() async throws {
|
||||
@Test
|
||||
func allow() async throws {
|
||||
// When allowing Touch/Face ID.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
|
||||
context.send(viewAction: .allow)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should now have biometric unlock enabled.
|
||||
XCTAssertEqual(appLockService.enableBiometricUnlockCallsCount, 1)
|
||||
#expect(appLockService.enableBiometricUnlockCallsCount == 1)
|
||||
}
|
||||
|
||||
func testSkip() async throws {
|
||||
@Test
|
||||
func skip() async throws {
|
||||
// When skipping biometrics.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .continue }
|
||||
context.send(viewAction: .skip)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the service should now have biometric unlock enabled.
|
||||
XCTAssertEqual(appLockService.enableBiometricUnlockCallsCount, 0)
|
||||
#expect(appLockService.enableBiometricUnlockCallsCount == 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class AppLockSetupPINScreenViewModelTests {
|
||||
var appLockService: AppLockService!
|
||||
var keychainController: KeychainControllerMock!
|
||||
var viewModel: AppLockSetupPINScreenViewModelProtocol!
|
||||
@@ -19,42 +20,40 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.resetAllSettings()
|
||||
keychainController = KeychainControllerMock()
|
||||
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testCreatePIN() async throws {
|
||||
@Test
|
||||
func createPIN() async throws {
|
||||
setup(mode: .create)
|
||||
|
||||
// Given the screen in create mode.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
|
||||
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
|
||||
#expect(context.viewState.mode == .create, "The mode should start as creation.")
|
||||
|
||||
// When entering an new PIN.
|
||||
let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm }
|
||||
let createDeferred = deferFulfillment(context.$viewState) { $0.mode == .confirm }
|
||||
context.pinCode = "2023"
|
||||
try await createDeferred.fulfill()
|
||||
|
||||
// Then the screen should transition to the confirm mode.
|
||||
XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.")
|
||||
#expect(context.viewState.mode == .confirm, "The mode should transition to confirmation.")
|
||||
|
||||
// When re-entering that PIN.
|
||||
let confirmDeferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete }
|
||||
let confirmDeferred = deferFulfillment(viewModel.actions) { $0 == .complete }
|
||||
context.pinCode = "2023"
|
||||
|
||||
// Then the screen should signal it is complete.
|
||||
try await confirmDeferred.fulfill()
|
||||
}
|
||||
|
||||
func testCreateWeakPIN() async throws {
|
||||
@Test
|
||||
func createWeakPIN() async throws {
|
||||
setup(mode: .create)
|
||||
|
||||
// Given the screen in create mode.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
|
||||
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
|
||||
#expect(context.viewState.mode == .create, "The mode should start as creation.")
|
||||
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
|
||||
|
||||
// When entering a weak PIN on the blocklist.
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
@@ -62,22 +61,24 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the PIN should be rejected and the user alerted.
|
||||
XCTAssertEqual(context.alertInfo?.id, .weakPIN, "The weak PIN should be rejected.")
|
||||
XCTAssertEqual(context.viewState.mode, .create, "The mode shouldn't transition after an invalid PIN code.")
|
||||
#expect(context.alertInfo?.id == .weakPIN, "The weak PIN should be rejected.")
|
||||
#expect(context.viewState.mode == .create, "The mode shouldn't transition after an invalid PIN code.")
|
||||
}
|
||||
|
||||
func testCreatePINMismatch() async throws {
|
||||
// Given the confirm mode after entering a new PIN.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, isMandatory: false, appLockService: appLockService)
|
||||
XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.")
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
|
||||
@Test
|
||||
func createPINMismatch() async throws {
|
||||
setup(mode: .create)
|
||||
|
||||
let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm }
|
||||
// Given the confirm mode after entering a new PIN.
|
||||
#expect(context.viewState.mode == .create, "The mode should start as creation.")
|
||||
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
|
||||
|
||||
let createDeferred = deferFulfillment(context.$viewState) { $0.mode == .confirm }
|
||||
context.pinCode = "2023"
|
||||
try await createDeferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.")
|
||||
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 0, "The mode should start with zero attempts.")
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert after a valid initial PIN.")
|
||||
#expect(context.viewState.mode == .confirm, "The mode should transition to confirmation.")
|
||||
#expect(context.viewState.numberOfConfirmAttempts == 0, "The mode should start with zero attempts.")
|
||||
#expect(context.alertInfo == nil, "There shouldn't be an alert after a valid initial PIN.")
|
||||
|
||||
// When entering the new PIN incorrectly
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 1 }
|
||||
@@ -85,8 +86,8 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the user should be alerted.
|
||||
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 1, "The mismatch should be counted.")
|
||||
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.")
|
||||
#expect(context.viewState.numberOfConfirmAttempts == 1, "The mismatch should be counted.")
|
||||
#expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
|
||||
|
||||
// When dismissing the alert and repeating twice more.
|
||||
context.alertInfo?.primaryButton.action?()
|
||||
@@ -97,42 +98,46 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
deferred = deferFulfillment(context.$viewState) { $0.numberOfConfirmAttempts == 3 }
|
||||
context.pinCode = "2024"
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.numberOfConfirmAttempts, 3, "All the mismatches should be counted.")
|
||||
XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.")
|
||||
#expect(context.viewState.numberOfConfirmAttempts == 3, "All the mismatches should be counted.")
|
||||
#expect(context.alertInfo?.id == .pinMismatch, "A PIN mismatch should be rejected.")
|
||||
|
||||
// Then tapping the alert button should reset back to create mode.
|
||||
context.alertInfo?.primaryButton.action?()
|
||||
XCTAssertEqual(context.viewState.mode, .create, "The mode should revert back to creation.")
|
||||
#expect(context.viewState.mode == .create, "The mode should revert back to creation.")
|
||||
}
|
||||
|
||||
func testUnlock() async throws {
|
||||
@Test
|
||||
func unlock() async throws {
|
||||
setup(mode: .unlock)
|
||||
|
||||
// Given the screen in unlock mode.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
|
||||
let pinCode = "2023"
|
||||
keychainController.pinCodeReturnValue = pinCode
|
||||
keychainController.containsPINCodeReturnValue = true
|
||||
keychainController.containsPINCodeBiometricStateReturnValue = false
|
||||
|
||||
// When entering the configured PIN.
|
||||
let deferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete }
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .complete }
|
||||
context.pinCode = pinCode
|
||||
|
||||
// Then the screen should signal it is complete.
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testForgotPIN() async throws {
|
||||
@Test
|
||||
func forgotPIN() async throws {
|
||||
setup(mode: .unlock)
|
||||
|
||||
// Given the screen in unlock mode.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.")
|
||||
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.")
|
||||
#expect(context.alertInfo == nil, "There shouldn't be an alert to begin with.")
|
||||
#expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
|
||||
|
||||
// When the user has forgotten their PIN.
|
||||
context.send(viewAction: .forgotPIN)
|
||||
|
||||
// Then an alert should be shown before logging out.
|
||||
XCTAssertEqual(context.alertInfo?.id, .confirmResetPIN, "The weak PIN should be rejected.")
|
||||
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not be disabled until the user confirms.")
|
||||
#expect(context.alertInfo?.id == .confirmResetPIN, "The weak PIN should be rejected.")
|
||||
#expect(!context.viewState.isLoggingOut, "The view should not be disabled until the user confirms.")
|
||||
|
||||
// When confirming the logout.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .forceLogout }
|
||||
@@ -140,44 +145,52 @@ class AppLockSetupPINScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Then a force logout should be initiated.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(context.viewState.isLoggingOut, "The view should become disabled.")
|
||||
#expect(context.viewState.isLoggingOut, "The view should become disabled.")
|
||||
}
|
||||
|
||||
func testUnlockFailed() async throws {
|
||||
@Test
|
||||
func unlockFailed() async throws {
|
||||
setup(mode: .unlock)
|
||||
|
||||
// Given the screen in unlock mode.
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, isMandatory: false, appLockService: appLockService)
|
||||
keychainController.pinCodeReturnValue = "2023"
|
||||
keychainController.containsPINCodeReturnValue = true
|
||||
keychainController.containsPINCodeBiometricStateReturnValue = false
|
||||
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 0, "The screen should start with zero attempts.")
|
||||
XCTAssertFalse(context.viewState.isSubtitleWarning, "The subtitle should start without a warning.")
|
||||
XCTAssertFalse(context.viewState.isLoggingOut, "The view should not start disabled.")
|
||||
#expect(context.viewState.numberOfUnlockAttempts == 0, "The screen should start with zero attempts.")
|
||||
#expect(!context.viewState.isSubtitleWarning, "The subtitle should start without a warning.")
|
||||
#expect(!context.viewState.isLoggingOut, "The view should not start disabled.")
|
||||
|
||||
// When entering a different PIN.
|
||||
var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
|
||||
message: "The PIN should be entered and then cleared by the view model.")
|
||||
var deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
|
||||
context.pinCode = "2024"
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the PIN should be rejected and the user notified.
|
||||
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 1, "An invalid attempt should be counted.")
|
||||
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.")
|
||||
XCTAssertFalse(context.viewState.isLoggingOut, "The view should still work.")
|
||||
#expect(context.viewState.numberOfUnlockAttempts == 1, "An invalid attempt should be counted.")
|
||||
#expect(context.viewState.isSubtitleWarning, "The subtitle should then show a warning.")
|
||||
#expect(!context.viewState.isLoggingOut, "The view should still work.")
|
||||
|
||||
// When entering the same incorrect PIN twice more
|
||||
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
|
||||
message: "The PIN should be entered and then cleared by the view model.")
|
||||
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
|
||||
context.pinCode = "2024"
|
||||
try await deferred.fulfill()
|
||||
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""],
|
||||
message: "The PIN should be entered and then cleared by the view model.")
|
||||
deferred = deferFulfillment(context.$viewState, keyPath: \.bindings.pinCode, transitionValues: ["", "2024", ""])
|
||||
context.pinCode = "2024"
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the user should be alerted that they're being signed out.
|
||||
XCTAssertEqual(context.viewState.numberOfUnlockAttempts, 3, "All invalid attempts should be counted.")
|
||||
XCTAssertTrue(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.")
|
||||
XCTAssertEqual(context.alertInfo?.id, .forceLogout, "An alert should be shown about a force logout.")
|
||||
XCTAssertTrue(context.viewState.isLoggingOut, "The view should become disabled.")
|
||||
#expect(context.viewState.numberOfUnlockAttempts == 3, "All invalid attempts should be counted.")
|
||||
#expect(context.viewState.isSubtitleWarning, "The subtitle should continue showing a warning.")
|
||||
#expect(context.alertInfo?.id == .forceLogout, "An alert should be shown about a force logout.")
|
||||
#expect(context.viewState.isLoggingOut, "The view should become disabled.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setup(mode: AppLockSetupPINScreenMode) {
|
||||
AppSettings.resetAllSettings()
|
||||
keychainController = KeychainControllerMock()
|
||||
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
|
||||
viewModel = AppLockSetupPINScreenViewModel(initialMode: mode, isMandatory: false, appLockService: appLockService)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,150 +7,155 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class AppLockTimerTests: XCTestCase {
|
||||
var timer: AppLockTimer!
|
||||
|
||||
let now = Date.now
|
||||
@Suite
|
||||
struct AppLockTimerTests {
|
||||
private let now = Date.now
|
||||
private var timer: AppLockTimer!
|
||||
|
||||
var gracePeriod: TimeInterval {
|
||||
timer.gracePeriod
|
||||
}
|
||||
|
||||
|
||||
var halfGracePeriod: TimeInterval {
|
||||
gracePeriod / 2
|
||||
timer.gracePeriod / 2
|
||||
}
|
||||
|
||||
|
||||
var gracePeriodX2: TimeInterval {
|
||||
gracePeriod * 2
|
||||
timer.gracePeriod * 2
|
||||
}
|
||||
|
||||
|
||||
var gracePeriodX10: TimeInterval {
|
||||
gracePeriod * 10
|
||||
timer.gracePeriod * 10
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
timer = nil
|
||||
@Test
|
||||
mutating func timerLockedOnStartup() {
|
||||
setupTimer(unlocked: false)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now),
|
||||
"The app should be locked on a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should be locked after a fresh launch.")
|
||||
}
|
||||
|
||||
func testTimerLockedOnStartup() {
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
|
||||
"The app should be locked on a fresh launch.")
|
||||
@Test
|
||||
mutating func timerBeforeFirstUnlock() {
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should be locked after a fresh launch.")
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should be locked after a fresh launch.")
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should be locked after a fresh launch.")
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
}
|
||||
|
||||
func testTimerBeforeFirstUnlock() {
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
|
||||
setupTimer(unlocked: false, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should always remain locked after backgrounding when locked.")
|
||||
}
|
||||
|
||||
func testTimerWhenUnlocked() {
|
||||
@Test
|
||||
mutating func timerWhenUnlocked() {
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||
}
|
||||
|
||||
func testTimerRepeatingWithinGracePeriod() {
|
||||
@Test
|
||||
mutating func timerRepeatingWithinGracePeriod() {
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
|
||||
var nextCheck = now + halfGracePeriod
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||
timer.applicationDidEnterBackground(date: nextCheck)
|
||||
|
||||
nextCheck = now + gracePeriod
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
timer.applicationDidEnterBackground(date: nextCheck)
|
||||
|
||||
nextCheck = now + gracePeriod + halfGracePeriod
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
timer.applicationDidEnterBackground(date: nextCheck)
|
||||
|
||||
nextCheck = now + gracePeriodX2
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||
timer.applicationDidEnterBackground(date: nextCheck)
|
||||
|
||||
nextCheck = now + gracePeriodX10
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
|
||||
}
|
||||
|
||||
func testTimerWithLongForeground() {
|
||||
@Test
|
||||
mutating func timerWithLongForeground() {
|
||||
setupTimer(unlocked: true)
|
||||
|
||||
let backgroundDate = now + gracePeriodX10
|
||||
timer.applicationDidEnterBackground(date: backgroundDate)
|
||||
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: backgroundDate + 1),
|
||||
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: backgroundDate + 1),
|
||||
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
|
||||
}
|
||||
|
||||
func testChangingTimeLocksApp() {
|
||||
@Test
|
||||
mutating func changingTimeLocksApp() {
|
||||
setupTimer(unlocked: true, backgroundedAt: now)
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now - 1),
|
||||
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now - 1),
|
||||
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
||||
}
|
||||
|
||||
func testNoGracePeriod() {
|
||||
@Test
|
||||
mutating func noGracePeriod() {
|
||||
// Given a timer with no grace period that is in the background.
|
||||
setupTimer(gracePeriod: 0, unlocked: true)
|
||||
let backgroundDate = now + 1
|
||||
timer.applicationDidEnterBackground(date: backgroundDate)
|
||||
|
||||
// Then the app should be locked immediately.
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: backgroundDate))
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: backgroundDate))
|
||||
}
|
||||
|
||||
func testResignActive() {
|
||||
@Test
|
||||
mutating func resignActive() {
|
||||
// Given a timer with no grace period.
|
||||
setupTimer(gracePeriod: 0, unlocked: true)
|
||||
|
||||
@@ -158,36 +163,32 @@ class AppLockTimerTests: XCTestCase {
|
||||
timer.applicationDidEnterBackground(date: now)
|
||||
|
||||
// Then the app should be locked.
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1))
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + 1))
|
||||
|
||||
// When the app resigns active but doesn't enter the background.
|
||||
// (Nothing to do here, we just don't call applicationDidEnterBackground).
|
||||
|
||||
// Then the app should also remain locked.
|
||||
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 2))
|
||||
#expect(timer.computeLockState(didBecomeActiveAt: now + 2))
|
||||
|
||||
// When unlocking the app and resigning active (but not entering the background)
|
||||
timer.registerUnlock()
|
||||
// (Again, nothing to do here for resigning active)
|
||||
|
||||
// Then the app should not become locked.
|
||||
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 3))
|
||||
#expect(!timer.computeLockState(didBecomeActiveAt: now + 3))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Sets up the timer for testing.
|
||||
/// - Parameters:
|
||||
/// - gracePeriod: Set up the test with a custom grace period for the timer. Defaults to 3 minutes.
|
||||
/// - unlocked: Whether the timer should consider itself unlocked or not.
|
||||
/// - backgroundedDate: If not nil, the timer will consider the app to have been backgrounded at the specified date.
|
||||
private func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
||||
timer = AppLockTimer(gracePeriod: gracePeriod)
|
||||
private mutating func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
||||
let timer = AppLockTimer(gracePeriod: gracePeriod)
|
||||
if unlocked {
|
||||
timer.registerUnlock()
|
||||
}
|
||||
if let backgroundedDate {
|
||||
timer.applicationDidEnterBackground(date: backgroundedDate)
|
||||
}
|
||||
self.timer = timer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class PINTextFieldTests: XCTestCase {
|
||||
func testSanitize() {
|
||||
@Suite
|
||||
struct PINTextFieldTests {
|
||||
@Test
|
||||
func sanitize() {
|
||||
let textField = PINTextField(pinCode: .constant(""))
|
||||
XCTAssertEqual(textField.sanitize("2"), "2")
|
||||
XCTAssertEqual(textField.sanitize("2023"), "2023")
|
||||
XCTAssertEqual(textField.sanitize("20233"), "2023")
|
||||
XCTAssertEqual(textField.sanitize("20x"), "20")
|
||||
XCTAssertEqual(textField.sanitize("20!"), "20")
|
||||
XCTAssertEqual(textField.sanitize("boop"), "")
|
||||
#expect(textField.sanitize("2") == "2")
|
||||
#expect(textField.sanitize("2023") == "2023")
|
||||
#expect(textField.sanitize("20233") == "2023")
|
||||
#expect(textField.sanitize("20x") == "20")
|
||||
#expect(textField.sanitize("20!") == "20")
|
||||
#expect(textField.sanitize("boop") == "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,117 +7,94 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class AppRouteURLParserTests: XCTestCase {
|
||||
var appSettings: AppSettings!
|
||||
var appRouteURLParser: AppRouteURLParser!
|
||||
@Suite
|
||||
struct AppRouteURLParserTests {
|
||||
var appSettings: AppSettings
|
||||
var appRouteURLParser: AppRouteURLParser
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
appRouteURLParser = AppRouteURLParser(appSettings: appSettings)
|
||||
}
|
||||
|
||||
func testElementCallRoutes() {
|
||||
guard let url = URL(string: "https://call.element.io/test") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func elementCallRoutes() throws {
|
||||
let url = try #require(URL(string: "https://call.element.io/test"))
|
||||
|
||||
XCTAssertEqual(appRouteURLParser.route(from: url), AppRoute.genericCallLink(url: url))
|
||||
#expect(appRouteURLParser.route(from: url) == AppRoute.genericCallLink(url: url))
|
||||
|
||||
guard let customSchemeURL = URL(string: "io.element.call:/?url=https%3A%2F%2Fcall.element.io%2Ftest") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=https%3A%2F%2Fcall.element.io%2Ftest"))
|
||||
|
||||
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), AppRoute.genericCallLink(url: url))
|
||||
#expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
|
||||
}
|
||||
|
||||
func testCustomDomainUniversalLinkCallRoutes() {
|
||||
guard let url = URL(string: "https://somecustomdomain.element.io/test") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func customDomainUniversalLinkCallRoutes() throws {
|
||||
let url = try #require(URL(string: "https://somecustomdomain.element.io/test"))
|
||||
|
||||
XCTAssertEqual(appRouteURLParser.route(from: url), nil)
|
||||
#expect(appRouteURLParser.route(from: url) == nil)
|
||||
}
|
||||
|
||||
func testCustomSchemeLinkCallRoutes() {
|
||||
@Test
|
||||
func customSchemeLinkCallRoutes() throws {
|
||||
let urlString = "https://somecustomdomain.element.io/test?param=123"
|
||||
guard let url = URL(string: urlString) else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
let url = try #require(URL(string: urlString))
|
||||
|
||||
guard let encodedURLString = urlString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
|
||||
XCTFail("Could not encode URL string")
|
||||
return
|
||||
}
|
||||
let encodedURLString = try #require(urlString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed))
|
||||
|
||||
guard let customSchemeURL = URL(string: "io.element.call:/?url=\(encodedURLString)") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=\(encodedURLString)"))
|
||||
|
||||
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), AppRoute.genericCallLink(url: url))
|
||||
#expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
|
||||
}
|
||||
|
||||
func testHttpCustomSchemeLinkCallRoutes() {
|
||||
guard let customSchemeURL = URL(string: "io.element.call:/?url=http%3A%2F%2Fcall.element.io%2Ftest") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
func httpCustomSchemeLinkCallRoutes() throws {
|
||||
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=http%3A%2F%2Fcall.element.io%2Ftest"))
|
||||
|
||||
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), nil)
|
||||
#expect(appRouteURLParser.route(from: customSchemeURL) == nil)
|
||||
}
|
||||
|
||||
func testMatrixUserURL() {
|
||||
@Test
|
||||
func matrixUserURL() throws {
|
||||
let userID = "@test:matrix.org"
|
||||
guard let url = URL(string: "https://matrix.to/#/\(userID)") else {
|
||||
XCTFail("Invalid url")
|
||||
return
|
||||
}
|
||||
let url = try #require(URL(string: "https://matrix.to/#/\(userID)"))
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .userProfile(userID: userID))
|
||||
#expect(route == .userProfile(userID: userID))
|
||||
}
|
||||
|
||||
func testMatrixRoomIdentifierURL() {
|
||||
@Test
|
||||
func matrixRoomIdentifierURL() throws {
|
||||
let id = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org"
|
||||
guard let url = URL(string: "https://matrix.to/#/\(id)") else {
|
||||
XCTFail("Invalid url")
|
||||
return
|
||||
}
|
||||
let url = try #require(URL(string: "https://matrix.to/#/\(id)"))
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .room(roomID: id, via: []))
|
||||
#expect(route == .room(roomID: id, via: []))
|
||||
}
|
||||
|
||||
func testWebRoomIDURL() {
|
||||
@Test
|
||||
func webRoomIDURL() throws {
|
||||
let id = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org"
|
||||
guard let url = URL(string: "https://app.element.io/#/room/\(id)") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
let url = try #require(URL(string: "https://app.element.io/#/room/\(id)"))
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .room(roomID: id, via: []))
|
||||
#expect(route == .room(roomID: id, via: []))
|
||||
}
|
||||
|
||||
func testWebUserIDURL() {
|
||||
@Test
|
||||
func webUserIDURL() throws {
|
||||
let id = "@alice:matrix.org"
|
||||
guard let url = URL(string: "https://develop.element.io/#/user/\(id)") else {
|
||||
XCTFail("URL invalid")
|
||||
return
|
||||
}
|
||||
let url = try #require(URL(string: "https://develop.element.io/#/user/\(id)"))
|
||||
|
||||
let route = appRouteURLParser.route(from: url)
|
||||
|
||||
XCTAssertEqual(route, .userProfile(userID: id))
|
||||
#expect(route == .userProfile(userID: id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,30 @@
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class ArrayTests: XCTestCase {
|
||||
func testGrouping() {
|
||||
XCTAssertEqual([].groupBy { $0 == 0 }, [])
|
||||
@Suite
|
||||
struct ArrayTests {
|
||||
@Test
|
||||
func grouping() {
|
||||
#expect([].groupBy { $0 == 0 } == [])
|
||||
|
||||
XCTAssertEqual([0].groupBy { $0 == 0 }, [[0]])
|
||||
#expect([0].groupBy { $0 == 0 } == [[0]])
|
||||
|
||||
XCTAssertEqual([1].groupBy { $0 == 0 }, [[1]])
|
||||
#expect([1].groupBy { $0 == 0 } == [[1]])
|
||||
|
||||
XCTAssertEqual([0, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0]])
|
||||
#expect([0, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0]])
|
||||
|
||||
XCTAssertEqual([1, 1, 1].groupBy { $0 == 0 }, [[1], [1], [1]])
|
||||
#expect([1, 1, 1].groupBy { $0 == 0 } == [[1], [1], [1]])
|
||||
|
||||
XCTAssertEqual([1, 0, 0, 1].groupBy { $0 == 0 }, [[1], [0, 0], [1]])
|
||||
#expect([1, 0, 0, 1].groupBy { $0 == 0 } == [[1], [0, 0], [1]])
|
||||
|
||||
XCTAssertEqual([0, 0, 1, 0].groupBy { $0 == 0 }, [[0, 0], [1], [0]])
|
||||
#expect([0, 0, 1, 0].groupBy { $0 == 0 } == [[0, 0], [1], [0]])
|
||||
|
||||
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0]])
|
||||
#expect([0, 0, 0, 1, 2, 3, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [2], [3], [0]])
|
||||
|
||||
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0, 0]])
|
||||
#expect([0, 0, 0, 1, 2, 3, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [2], [3], [0, 0]])
|
||||
|
||||
XCTAssertEqual([0, 0, 0, 1, 0, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [0], [2], [3], [0, 0]])
|
||||
#expect([0, 0, 0, 1, 0, 2, 3, 0, 0].groupBy { $0 == 0 } == [[0, 0, 0], [1], [0], [2], [3], [0, 0]])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
||||
checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
|
||||
}
|
||||
|
||||
|
||||
func testDefaultFont() {
|
||||
let htmlString = "<b>Test</b> <i>string</i> "
|
||||
|
||||
@@ -313,7 +313,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
|
||||
XCTFail("Couldn't find blockquote")
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:enable line_length
|
||||
|
||||
func testBlockquoteWithLink() {
|
||||
@@ -515,7 +515,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1)
|
||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||
checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1)
|
||||
|
||||
|
||||
let string2 = "Hello @room"
|
||||
let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
|
||||
checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2)
|
||||
@@ -824,7 +824,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
XCTFail("Could not build the attributed string")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
||||
XCTFail("Couldn't find the link")
|
||||
return
|
||||
@@ -840,7 +840,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
XCTFail("Could not build the attributed string")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let link = attributedString.runs.first(where: { $0.link != nil })?.link else {
|
||||
XCTFail("Couldn't find the link")
|
||||
return
|
||||
@@ -1108,7 +1108,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
XCTAssertEqual(link.confirmationParameters?.internalURL.absoluteString, "https://matrix.org")
|
||||
XCTAssertEqual(link.confirmationParameters?.displayString, "👉️ #room:matrix.org")
|
||||
}
|
||||
|
||||
|
||||
func testMxExternalPaymentDetailsRemoved() {
|
||||
var htmlString = "This is visible.<span data-msc4286-external-payment-details> But this is hidden <a href=\"https://matrix.org\">and this link too</a></span>"
|
||||
|
||||
@@ -1138,7 +1138,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||
|
||||
@@ -7,31 +7,30 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class AttributedStringTests: XCTestCase {
|
||||
func testReplacingFontWithPresentationIntent() {
|
||||
@Suite
|
||||
struct AttributedStringTests {
|
||||
@Test
|
||||
func replacingFontWithPresentationIntent() throws {
|
||||
// Given a string parsed from HTML that contains specific fixed size fonts.
|
||||
let boldString = "Bold"
|
||||
guard let originalString = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||
.fromHTML("Normal <b>\(boldString)</b> Normal.") else {
|
||||
XCTFail("The attributed string should be built from the HTML.")
|
||||
return
|
||||
}
|
||||
let originalString = try #require(AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||
.fromHTML("Normal <b>\(boldString)</b> Normal."))
|
||||
|
||||
// When replacing the font with a presentation intent.
|
||||
let string = originalString.replacingFontWithPresentationIntent()
|
||||
|
||||
// Then the font should be removed with an inline presentation intent applied to the bold text.
|
||||
for run in string.runs {
|
||||
XCTAssertNil(run.uiKit.font, "The UIFont should have been removed.")
|
||||
XCTAssertNil(run.font, "No font should be in the run at all.")
|
||||
#expect(run.uiKit.font == nil, "The UIFont should have been removed.")
|
||||
#expect(run.font == nil, "No font should be in the run at all.")
|
||||
|
||||
let substring = string[run.range]
|
||||
if String(substring.characters) == boldString {
|
||||
XCTAssertEqual(run.inlinePresentationIntent, .stronglyEmphasized, "The bold string should be bold.")
|
||||
#expect(run.inlinePresentationIntent == .stronglyEmphasized, "The bold string should be bold.")
|
||||
} else {
|
||||
XCTAssertNil(run.presentationIntent, "The rest should be plain.")
|
||||
#expect(run.presentationIntent == nil, "The rest should be plain.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AudioPlayerStateTests: XCTestCase {
|
||||
@Suite
|
||||
struct AudioPlayerStateTests {
|
||||
static let audioDuration = 10.0
|
||||
private var audioPlayerState: AudioPlayerState!
|
||||
private var audioPlayerMock: AudioPlayerMock!
|
||||
@@ -36,39 +37,42 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
return audioPlayerMock
|
||||
}
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async {
|
||||
audioPlayerActionsSubject = .init()
|
||||
audioPlayerSeekCallsSubject = .init()
|
||||
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: Self.audioDuration)
|
||||
audioPlayerMock = buildAudioPlayerMock()
|
||||
audioPlayerMock.seekToClosure = { [weak self] progress in
|
||||
self?.audioPlayerMock.currentTime = Self.audioDuration * progress
|
||||
audioPlayerMock.seekToClosure = { [audioPlayerMock] progress in
|
||||
audioPlayerMock?.currentTime = Self.audioDuration * progress
|
||||
}
|
||||
}
|
||||
|
||||
func testAttach() {
|
||||
@Test
|
||||
func attach() {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
XCTAssert(audioPlayerState.isAttached)
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .loading)
|
||||
#expect(audioPlayerState.isAttached)
|
||||
#expect(audioPlayerState.playbackState == .loading)
|
||||
}
|
||||
|
||||
func testDetach() {
|
||||
@Test
|
||||
mutating func detach() {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
audioPlayerState.detachAudioPlayer()
|
||||
XCTAssert(audioPlayerMock.stopCalled)
|
||||
XCTAssertFalse(audioPlayerState.isAttached)
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
|
||||
XCTAssertFalse(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerMock.stopCalled)
|
||||
#expect(!audioPlayerState.isAttached)
|
||||
#expect(audioPlayerState.playbackState == .stopped)
|
||||
#expect(!audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
|
||||
func testDelayedState() async throws {
|
||||
@Test
|
||||
func delayedState() async throws {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
XCTAssert(audioPlayerState.isAttached)
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .loading)
|
||||
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped)
|
||||
#expect(audioPlayerState.isAttached)
|
||||
#expect(audioPlayerState.playbackState == .loading)
|
||||
#expect(audioPlayerState.playerButtonPlaybackState == .stopped)
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
|
||||
switch output {
|
||||
@@ -80,13 +84,14 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .loading)
|
||||
#expect(audioPlayerState.playerButtonPlaybackState == .loading)
|
||||
}
|
||||
|
||||
func testOtherActionsAreNotDelayed() async throws {
|
||||
@Test
|
||||
func otherActionsAreNotDelayed() async throws {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .loading)
|
||||
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .stopped)
|
||||
#expect(audioPlayerState.playbackState == .loading)
|
||||
#expect(audioPlayerState.playerButtonPlaybackState == .stopped)
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playerButtonPlaybackState) { output in
|
||||
switch output {
|
||||
@@ -99,53 +104,48 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didStartPlaying)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .playing)
|
||||
XCTAssertEqual(audioPlayerState.playerButtonPlaybackState, .playing)
|
||||
#expect(audioPlayerState.playbackState == .playing)
|
||||
#expect(audioPlayerState.playerButtonPlaybackState == .playing)
|
||||
}
|
||||
|
||||
func testReportError() {
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
|
||||
@Test
|
||||
mutating func reportError() {
|
||||
#expect(audioPlayerState.playbackState == .stopped)
|
||||
audioPlayerState.reportError()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .error)
|
||||
#expect(audioPlayerState.playbackState == .error)
|
||||
}
|
||||
|
||||
func testUpdateProgress() async {
|
||||
@Test
|
||||
mutating func updateProgress() async {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
// If we try to set a negative progress, the new progress must be 0.0
|
||||
do {
|
||||
await audioPlayerState.updateState(progress: -5.0)
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.0)
|
||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.0)
|
||||
}
|
||||
|
||||
// If we try to set a progress > 1.0, the new progress must be 1.0
|
||||
do {
|
||||
await audioPlayerState.updateState(progress: 1.5)
|
||||
XCTAssertEqual(audioPlayerState.progress, 1.0)
|
||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 1.0)
|
||||
}
|
||||
|
||||
do {
|
||||
audioPlayerMock.state = .stopped
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
|
||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
||||
}
|
||||
|
||||
do {
|
||||
audioPlayerMock.state = .playing
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
|
||||
XCTAssert(audioPlayerState.isPublishingProgress)
|
||||
}
|
||||
// If we try to set a negative progress, the new progress must be 0.0
|
||||
await audioPlayerState.updateState(progress: -5.0)
|
||||
#expect(audioPlayerState.progress == 0.0)
|
||||
#expect(audioPlayerMock.seekToReceivedProgress == 0.0)
|
||||
|
||||
// If we try to set a progress > 1.0, the new progress must be 1.0
|
||||
await audioPlayerState.updateState(progress: 1.5)
|
||||
#expect(audioPlayerState.progress == 1.0)
|
||||
#expect(audioPlayerMock.seekToReceivedProgress == 1.0)
|
||||
|
||||
audioPlayerMock.state = .stopped
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
#expect(audioPlayerState.progress == 0.4)
|
||||
#expect(audioPlayerMock.seekToReceivedProgress == 0.4)
|
||||
#expect(!audioPlayerState.isPublishingProgress)
|
||||
|
||||
audioPlayerMock.state = .playing
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
#expect(audioPlayerState.progress == 0.4)
|
||||
#expect(audioPlayerMock.seekToReceivedProgress == 0.4)
|
||||
#expect(audioPlayerState.isPublishingProgress)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionDidStartLoading() async throws {
|
||||
|
||||
@Test
|
||||
func handlingAudioPlayerActionDidStartLoading() async throws {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .loading:
|
||||
@@ -157,15 +157,16 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didStartLoading)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .loading)
|
||||
#expect(audioPlayerState.playbackState == .loading)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionDidFinishLoading() async throws {
|
||||
|
||||
@Test
|
||||
mutating func handlingAudioPlayerActionDidFinishLoading() async throws {
|
||||
audioPlayerMock.duration = 10.0
|
||||
|
||||
audioPlayerState = AudioPlayerState(id: .timelineItemIdentifier(.randomEvent), title: "", duration: 0)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .readyToPlay:
|
||||
@@ -179,15 +180,16 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// The state is expected to be .readyToPlay
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .readyToPlay)
|
||||
#expect(audioPlayerState.playbackState == .readyToPlay)
|
||||
// The duration should have been updated with the player's duration
|
||||
XCTAssertEqual(audioPlayerState.duration, audioPlayerMock.duration)
|
||||
#expect(audioPlayerState.duration == audioPlayerMock.duration)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionDidStartPlaying() async throws {
|
||||
@Test
|
||||
mutating func handlingAudioPlayerActionDidStartPlaying() async throws {
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .playing:
|
||||
@@ -199,16 +201,17 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didStartPlaying)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerMock.seekToReceivedProgress, 0.4)
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .playing)
|
||||
XCTAssert(audioPlayerState.isPublishingProgress)
|
||||
XCTAssert(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerMock.seekToReceivedProgress == 0.4)
|
||||
#expect(audioPlayerState.playbackState == .playing)
|
||||
#expect(audioPlayerState.isPublishingProgress)
|
||||
#expect(audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionDidPausePlaying() async throws {
|
||||
@Test
|
||||
mutating func handlingAudioPlayerActionDidPausePlaying() async throws {
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .stopped:
|
||||
@@ -220,16 +223,17 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didPausePlaying)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
||||
XCTAssert(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerState.playbackState == .stopped)
|
||||
#expect(audioPlayerState.progress == 0.4)
|
||||
#expect(!audioPlayerState.isPublishingProgress)
|
||||
#expect(audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionsidStopPlaying() async throws {
|
||||
@Test
|
||||
mutating func handlingAudioPlayerActionsidStopPlaying() async throws {
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .stopped:
|
||||
@@ -241,16 +245,17 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didStopPlaying)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.4)
|
||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
||||
XCTAssert(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerState.playbackState == .stopped)
|
||||
#expect(audioPlayerState.progress == 0.4)
|
||||
#expect(!audioPlayerState.isPublishingProgress)
|
||||
#expect(audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
|
||||
func testAudioPlayerActionsDidFinishPlaying() async throws {
|
||||
@Test
|
||||
mutating func audioPlayerActionsDidFinishPlaying() async throws {
|
||||
await audioPlayerState.updateState(progress: 0.4)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .stopped:
|
||||
@@ -262,16 +267,17 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didFinishPlaying)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .stopped)
|
||||
#expect(audioPlayerState.playbackState == .stopped)
|
||||
// Progress should be reset to 0
|
||||
XCTAssertEqual(audioPlayerState.progress, 0.0)
|
||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
||||
XCTAssertFalse(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerState.progress == 0.0)
|
||||
#expect(!audioPlayerState.isPublishingProgress)
|
||||
#expect(!audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
|
||||
func testAudioPlayerActionsDidFailed() async throws {
|
||||
@Test
|
||||
func audioPlayerActionsDidFailed() async throws {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayerMock)
|
||||
|
||||
|
||||
let deferredPlayingState = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .playing:
|
||||
@@ -282,8 +288,8 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
}
|
||||
audioPlayerActionsSubject.send(.didStartPlaying)
|
||||
try await deferredPlayingState.fulfill()
|
||||
XCTAssertFalse(audioPlayerState.showProgressIndicator)
|
||||
|
||||
#expect(!audioPlayerState.showProgressIndicator)
|
||||
|
||||
let deferred = deferFulfillment(audioPlayerState.$playbackState) { action in
|
||||
switch action {
|
||||
case .error:
|
||||
@@ -295,8 +301,8 @@ class AudioPlayerStateTests: XCTestCase {
|
||||
|
||||
audioPlayerActionsSubject.send(.didFailWithError(error: AudioPlayerError.genericError))
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioPlayerState.playbackState, .error)
|
||||
XCTAssertFalse(audioPlayerState.isPublishingProgress)
|
||||
XCTAssertFalse(audioPlayerState.showProgressIndicator)
|
||||
#expect(audioPlayerState.playbackState == .error)
|
||||
#expect(!audioPlayerState.isPublishingProgress)
|
||||
#expect(!audioPlayerState.showProgressIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AudioRecorderStateTests: XCTestCase {
|
||||
@Suite
|
||||
struct AudioRecorderStateTests {
|
||||
private var audioRecorderState: AudioRecorderState!
|
||||
private var audioRecorderMock: AudioRecorderMock!
|
||||
|
||||
@@ -20,7 +21,7 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
private var audioRecorderActions: AnyPublisher<AudioRecorderAction, Never> {
|
||||
audioRecorderActionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private func buildAudioRecorderMock() -> AudioRecorderMock {
|
||||
let audioRecorderMock = AudioRecorderMock()
|
||||
audioRecorderMock.isRecording = false
|
||||
@@ -30,34 +31,38 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
return audioRecorderMock
|
||||
}
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async {
|
||||
audioRecorderActionsSubject = .init()
|
||||
audioRecorderState = AudioRecorderState()
|
||||
audioRecorderMock = buildAudioRecorderMock()
|
||||
}
|
||||
|
||||
func testAttach() {
|
||||
@Test
|
||||
func attach() {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
#expect(audioRecorderState.recordingState == .stopped)
|
||||
}
|
||||
|
||||
func testDetach() async {
|
||||
@Test
|
||||
mutating func detach() async {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
audioRecorderMock.isRecording = true
|
||||
await audioRecorderState.detachAudioRecorder()
|
||||
XCTAssert(audioRecorderMock.stopRecordingCalled)
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
#expect(audioRecorderMock.stopRecordingCalled)
|
||||
#expect(audioRecorderState.recordingState == .stopped)
|
||||
}
|
||||
|
||||
func testReportError() {
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
@Test
|
||||
mutating func reportError() {
|
||||
#expect(audioRecorderState.recordingState == .stopped)
|
||||
audioRecorderState.reportError()
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .error)
|
||||
#expect(audioRecorderState.recordingState == .error)
|
||||
}
|
||||
|
||||
func testHandlingAudioRecorderActionDidStartRecording() async throws {
|
||||
@Test
|
||||
func handlingAudioRecorderActionDidStartRecording() async throws {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
|
||||
switch action {
|
||||
case .recording:
|
||||
@@ -69,12 +74,13 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
|
||||
audioRecorderActionsSubject.send(.didStartRecording)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .recording)
|
||||
#expect(audioRecorderState.recordingState == .recording)
|
||||
}
|
||||
|
||||
func testHandlingAudioPlayerActionDidStopRecording() async throws {
|
||||
|
||||
@Test
|
||||
func handlingAudioPlayerActionDidStopRecording() async throws {
|
||||
audioRecorderState.attachAudioRecorder(audioRecorderMock)
|
||||
|
||||
|
||||
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
|
||||
switch action {
|
||||
case .stopped:
|
||||
@@ -88,6 +94,6 @@ class AudioRecorderStateTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// The state is expected to be .readyToPlay
|
||||
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
|
||||
#expect(audioRecorderState.recordingState == .stopped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class AudioRecorderTests: XCTestCase {
|
||||
@Suite
|
||||
struct AudioRecorderTests {
|
||||
private var audioRecorder: AudioRecorder!
|
||||
private var audioSessionMock: AudioSessionMock!
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async {
|
||||
audioSessionMock = AudioSessionMock()
|
||||
audioSessionMock.requestRecordPermissionClosure = { completion in
|
||||
completion(true)
|
||||
@@ -24,11 +25,8 @@ class AudioRecorderTests: XCTestCase {
|
||||
audioRecorder = AudioRecorder(audioSession: audioSessionMock)
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
await audioRecorder?.cancelRecording()
|
||||
}
|
||||
|
||||
func testRecordWithoutPermission() async throws {
|
||||
@Test
|
||||
mutating func recordWithoutPermission() async throws {
|
||||
audioSessionMock.requestRecordPermissionClosure = { completion in
|
||||
completion(false)
|
||||
}
|
||||
@@ -44,6 +42,6 @@ class AudioRecorderTests: XCTestCase {
|
||||
let url = URL.temporaryDirectory.appendingPathComponent("test-voice-message").appendingPathExtension("m4a")
|
||||
await audioRecorder.record(audioFileURL: url)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(audioRecorder.isRecording)
|
||||
#expect(!audioRecorder.isRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,86 +7,93 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class AuthenticationServiceTests: XCTestCase {
|
||||
@Suite
|
||||
@MainActor
|
||||
struct AuthenticationServiceTests {
|
||||
var client: ClientSDKMock!
|
||||
var userSessionStore: UserSessionStoreMock!
|
||||
var encryptionKeyProvider: MockEncryptionKeyProvider!
|
||||
|
||||
var service: AuthenticationService!
|
||||
|
||||
func testPasswordLogin() async {
|
||||
setupMocks(serverAddress: "example.com")
|
||||
@Test
|
||||
mutating func passwordLogin() async {
|
||||
setup(serverAddress: "example.com")
|
||||
|
||||
switch await service.configure(for: "example.com", flow: .login) {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
XCTFail("Unexpected failure: \(error)")
|
||||
Issue.record("Unexpected failure: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertEqual(service.flow, .login)
|
||||
XCTAssertEqual(service.homeserver.value, .mockBasicServer)
|
||||
#expect(service.flow == .login)
|
||||
#expect(service.homeserver.value == .mockBasicServer)
|
||||
|
||||
switch await service.login(username: "alice", password: "12345678", initialDeviceName: nil, deviceID: nil) {
|
||||
case .success:
|
||||
XCTAssertEqual(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount, 1)
|
||||
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount, 1)
|
||||
XCTAssertEqual(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase,
|
||||
encryptionKeyProvider.generateKey().base64EncodedString())
|
||||
#expect(client.loginUsernamePasswordInitialDeviceNameDeviceIdCallsCount == 1)
|
||||
#expect(userSessionStore.userSessionForSessionDirectoriesPassphraseCallsCount == 1)
|
||||
#expect(userSessionStore.userSessionForSessionDirectoriesPassphraseReceivedArguments?.passphrase ==
|
||||
encryptionKeyProvider.generateKey().base64EncodedString())
|
||||
case .failure(let error):
|
||||
XCTFail("Unexpected failure: \(error)")
|
||||
Issue.record("Unexpected failure: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func testConfigureLoginWithOIDC() async {
|
||||
setupMocks()
|
||||
@Test
|
||||
mutating func configureLoginWithOIDC() async {
|
||||
setup()
|
||||
|
||||
switch await service.configure(for: "matrix.org", flow: .login) {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
XCTFail("Unexpected failure: \(error)")
|
||||
Issue.record("Unexpected failure: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertEqual(service.flow, .login)
|
||||
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
|
||||
#expect(service.flow == .login)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
func testConfigureRegisterWithOIDC() async {
|
||||
setupMocks()
|
||||
@Test
|
||||
mutating func configureRegisterWithOIDC() async {
|
||||
setup()
|
||||
|
||||
switch await service.configure(for: "matrix.org", flow: .register) {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
XCTFail("Unexpected failure: \(error)")
|
||||
Issue.record("Unexpected failure: \(error)")
|
||||
}
|
||||
|
||||
XCTAssertEqual(service.flow, .register)
|
||||
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
|
||||
#expect(service.flow == .register)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
func testConfigureRegisterNoSupport() async {
|
||||
@Test
|
||||
@MainActor
|
||||
mutating func configureRegisterNoSupport() async {
|
||||
let homeserverAddress = "example.com"
|
||||
setupMocks(serverAddress: homeserverAddress)
|
||||
setup(serverAddress: homeserverAddress)
|
||||
|
||||
switch await service.configure(for: homeserverAddress, flow: .register) {
|
||||
case .success:
|
||||
XCTFail("Configuration should have failed")
|
||||
Issue.record("Configuration should have failed")
|
||||
case .failure(let error):
|
||||
XCTAssertEqual(error, .registrationNotSupported)
|
||||
#expect(error == .registrationNotSupported)
|
||||
}
|
||||
|
||||
XCTAssertEqual(service.flow, .login)
|
||||
XCTAssertEqual(service.homeserver.value, .init(address: "matrix.org", loginMode: .unknown))
|
||||
#expect(service.flow == .login)
|
||||
#expect(service.homeserver.value == .init(address: "matrix.org", loginMode: .unknown))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupMocks(serverAddress: String = "matrix.org") {
|
||||
private mutating func setup(serverAddress: String = "matrix.org") {
|
||||
let configuration: AuthenticationClientFactoryMock.Configuration = .init()
|
||||
let clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class AuthenticationStartScreenViewModelTests {
|
||||
var clientFactory: AuthenticationClientFactoryMock!
|
||||
var client: ClientSDKMock!
|
||||
var appSettings: AppSettings!
|
||||
@@ -22,22 +24,23 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
// These app settings are kept local to the tests on purpose as if they are registered in the
|
||||
// ServiceLocator, the providers override that we apply will break other tests in the suite.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testInitialState() async throws {
|
||||
@Test
|
||||
func initialState() async throws {
|
||||
// Given a view model that has no provisioning parameters.
|
||||
setupViewModel()
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When tapping any of the buttons on the screen
|
||||
let actions: [(AuthenticationStartScreenViewAction, AuthenticationStartScreenViewModelAction)] = [
|
||||
@@ -53,17 +56,18 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the authentication service should not be used yet.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
}
|
||||
}
|
||||
|
||||
func testProvisionedOIDCState() async throws {
|
||||
@Test
|
||||
func provisionedOIDCState() async throws {
|
||||
// Given a view model that has been provisioned with a server that supports OIDC.
|
||||
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"))
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
@@ -71,18 +75,19 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .login)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint, "user@company.com")
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "user@company.com")
|
||||
#expect(authenticationService.homeserver.value.loginMode == .oidc(supportsCreatePrompt: false))
|
||||
}
|
||||
|
||||
func testProvisionedPasswordState() async throws {
|
||||
@Test
|
||||
func provisionedPasswordState() async throws {
|
||||
// Given a view model that has been provisioned with a server that does not support OIDC.
|
||||
setupViewModel(provisioningParameters: .init(accountProvider: "company.com", loginHint: "user@company.com"), supportsOIDC: false)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
@@ -91,16 +96,17 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
func testSingleProviderOIDCState() async throws {
|
||||
@Test
|
||||
func singleProviderOIDCState() async throws {
|
||||
// Given a view model that for an app that only allows the use of a single provider that supports OIDC.
|
||||
setAllowedAccountProviders(["company.com"])
|
||||
setupViewModel()
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
@@ -108,19 +114,20 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .login)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 1)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt, .consent)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint, nil)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .oidc(supportsCreatePrompt: false))
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 1)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.prompt == .consent)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == nil)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .oidc(supportsCreatePrompt: false))
|
||||
}
|
||||
|
||||
func testSingleProviderPasswordState() async throws {
|
||||
@Test
|
||||
func singleProviderPasswordState() async throws {
|
||||
// Given a view model that for an app that only allows the use of a single provider that does not support OIDC.
|
||||
setAllowedAccountProviders(["company.com"])
|
||||
setupViewModel(supportsOIDC: false)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount, 0)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .unknown)
|
||||
#expect(client.urlForOidcOidcConfigurationPromptLoginHintDeviceIdAdditionalScopesCallsCount == 0)
|
||||
|
||||
// When tapping the login button the authentication service should be used and the screen
|
||||
// should request to continue the flow without any server selection needed.
|
||||
@@ -129,8 +136,8 @@ class AuthenticationStartScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then a call to configure service should be made.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(authenticationService.homeserver.value.loginMode, .password)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(authenticationService.homeserver.value.loginMode == .password)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -8,25 +8,29 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class BlockedUsersScreenViewModelTests: XCTestCase {
|
||||
func testInitialState() async throws {
|
||||
@Suite
|
||||
struct BlockedUsersScreenViewModelTests {
|
||||
@Test
|
||||
func initialState() async throws {
|
||||
let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID))
|
||||
|
||||
let viewModel = BlockedUsersScreenViewModel(hideProfiles: true,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
|
||||
let deferred = deferFailure(viewModel.context.observe(\.viewState.blockedUsers), timeout: 1) { $0.contains { $0.displayName != nil } }
|
||||
let deferred = deferFailure(viewModel.context.observe(\.viewState.blockedUsers), timeout: .seconds(1)) { $0.contains { $0.displayName != nil } }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)
|
||||
XCTAssertFalse(clientProxy.profileForCalled)
|
||||
#expect(!viewModel.context.viewState.blockedUsers.isEmpty)
|
||||
#expect(!clientProxy.profileForCalled)
|
||||
}
|
||||
|
||||
func testProfiles() async throws {
|
||||
@Test
|
||||
func profiles() async throws {
|
||||
let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID))
|
||||
|
||||
let viewModel = BlockedUsersScreenViewModel(hideProfiles: false,
|
||||
@@ -36,7 +40,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.blockedUsers)) { $0.contains { $0.displayName != nil } }
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty)
|
||||
XCTAssertTrue(clientProxy.profileForCalled)
|
||||
#expect(!viewModel.context.viewState.blockedUsers.isEmpty)
|
||||
#expect(clientProxy.profileForCalled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,20 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class BugReportScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct BugReportScreenViewModelTests {
|
||||
let logFiles: [URL] = [URL(filePath: "/path/to/file1.log"), URL(filePath: "/path/to/file2.log")]
|
||||
|
||||
enum TestError: Error {
|
||||
case testError
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
@Test
|
||||
func initialState() {
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
|
||||
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
|
||||
clientProxy: clientProxy,
|
||||
@@ -26,12 +29,13 @@ class BugReportScreenViewModelTests: XCTestCase {
|
||||
isModallyPresented: false)
|
||||
let context = viewModel.context
|
||||
|
||||
XCTAssertEqual(context.reportText, "")
|
||||
XCTAssertNil(context.viewState.screenshot)
|
||||
XCTAssertTrue(context.sendingLogsEnabled)
|
||||
#expect(context.reportText == "")
|
||||
#expect(context.viewState.screenshot == nil)
|
||||
#expect(context.sendingLogsEnabled)
|
||||
}
|
||||
|
||||
func testClearScreenshot() {
|
||||
@Test
|
||||
func clearScreenshot() {
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
|
||||
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
|
||||
clientProxy: clientProxy,
|
||||
@@ -41,10 +45,11 @@ class BugReportScreenViewModelTests: XCTestCase {
|
||||
let context = viewModel.context
|
||||
|
||||
context.send(viewAction: .removeScreenshot)
|
||||
XCTAssertNil(context.viewState.screenshot)
|
||||
#expect(context.viewState.screenshot == nil)
|
||||
}
|
||||
|
||||
func testAttachScreenshot() {
|
||||
@Test
|
||||
func attachScreenshot() {
|
||||
let clientProxy = ClientProxyMock(.init(userID: "@mock.client.com"))
|
||||
let viewModel = BugReportScreenViewModel(bugReportService: BugReportServiceMock(),
|
||||
clientProxy: clientProxy,
|
||||
@@ -52,12 +57,13 @@ class BugReportScreenViewModelTests: XCTestCase {
|
||||
screenshot: nil,
|
||||
isModallyPresented: false)
|
||||
let context = viewModel.context
|
||||
XCTAssertNil(context.viewState.screenshot)
|
||||
#expect(context.viewState.screenshot == nil)
|
||||
context.send(viewAction: .attachScreenshot(UIImage.actions))
|
||||
XCTAssert(context.viewState.screenshot == UIImage.actions)
|
||||
#expect(context.viewState.screenshot == UIImage.actions)
|
||||
}
|
||||
|
||||
func testSendReportWithSuccess() async throws {
|
||||
@Test
|
||||
func sendReportWithSuccess() async throws {
|
||||
let mockService = BugReportServiceMock()
|
||||
mockService.submitBugReportProgressListenerClosure = { _, _ in
|
||||
await Task.yield()
|
||||
@@ -88,19 +94,20 @@ class BugReportScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .submit)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1)
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID, "@mock.client.com")
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID, "ABCDEFGH")
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.curve25519, "THECURVEKEYKEY")
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.ed25519, "THEEDKEYKEY")
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text, "This will succeed")
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.logFiles, logFiles)
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact, false)
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels, [])
|
||||
XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files, [])
|
||||
#expect(mockService.submitBugReportProgressListenerCallsCount == 1)
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID == "@mock.client.com")
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID == "ABCDEFGH")
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.curve25519 == "THECURVEKEYKEY")
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.ed25519 == "THEEDKEYKEY")
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text == "This will succeed")
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.logFiles == logFiles)
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact == false)
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels == [])
|
||||
#expect(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files == [])
|
||||
}
|
||||
|
||||
func testSendReportWithError() async throws {
|
||||
|
||||
@Test
|
||||
func sendReportWithError() async throws {
|
||||
let mockService = BugReportServiceMock()
|
||||
mockService.submitBugReportProgressListenerClosure = { _, _ in
|
||||
.failure(.uploadFailure(TestError.testError))
|
||||
@@ -125,8 +132,8 @@ class BugReportScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .submit)
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1)
|
||||
XCTAssertEqual(context.reportText, "This will fail", "The bug report should remain in place so the user can retry.")
|
||||
XCTAssertFalse(context.viewState.shouldDisableInteraction, "The user should be able to retry.")
|
||||
#expect(mockService.submitBugReportProgressListenerCallsCount == 1)
|
||||
#expect(context.reportText == "This will fail", "The bug report should remain in place so the user can retry.")
|
||||
#expect(!context.viewState.shouldDisableInteraction, "The user should be able to retry.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class BugReportServiceTests: XCTestCase {
|
||||
@Suite
|
||||
final class BugReportServiceTests {
|
||||
var appSettings: AppSettings!
|
||||
var bugReportService: BugReportServiceProtocol!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
|
||||
init() throws {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
appSettings.bugReportRageshakeURL.reset()
|
||||
@@ -26,15 +27,17 @@ class BugReportServiceTests: XCTestCase {
|
||||
bugReportService = bugReportServiceMock
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
appSettings.bugReportRageshakeURL.reset()
|
||||
}
|
||||
|
||||
func testInitialStateWithMockService() {
|
||||
XCTAssertFalse(bugReportService.crashedLastRun)
|
||||
|
||||
@Test
|
||||
func initialStateWithMockService() {
|
||||
#expect(!bugReportService.crashedLastRun)
|
||||
}
|
||||
|
||||
func testSubmitBugReportWithMockService() async throws {
|
||||
|
||||
@Test
|
||||
func submitBugReportWithMockService() async throws {
|
||||
let bugReport = BugReport(userID: "@mock:client.com",
|
||||
deviceID: nil,
|
||||
ed25519: nil,
|
||||
@@ -46,40 +49,43 @@ class BugReportServiceTests: XCTestCase {
|
||||
files: [])
|
||||
let progressSubject = CurrentValueSubject<Double, Never>(0.0)
|
||||
let response = try await bugReportService.submitBugReport(bugReport, progressListener: progressSubject).get()
|
||||
let reportURL = try XCTUnwrap(response.reportURL)
|
||||
XCTAssertFalse(reportURL.isEmpty)
|
||||
let reportURL = try #require(response.reportURL)
|
||||
#expect(!reportURL.isEmpty)
|
||||
}
|
||||
|
||||
func testInitialStateWithRealService() {
|
||||
@Test
|
||||
func initialStateWithRealService() {
|
||||
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit"))
|
||||
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
|
||||
applicationID: "mock_app_id",
|
||||
sdkGitSHA: "1234",
|
||||
session: .mock,
|
||||
appHooks: AppHooks())
|
||||
XCTAssertTrue(service.isEnabled)
|
||||
XCTAssertFalse(service.crashedLastRun)
|
||||
#expect(service.isEnabled)
|
||||
#expect(!service.crashedLastRun)
|
||||
}
|
||||
|
||||
func testInitialStateWithRealServiceAndDisabled() {
|
||||
@Test
|
||||
func initialStateWithRealServiceAndDisabled() {
|
||||
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.disabled)
|
||||
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
|
||||
applicationID: "mock_app_id",
|
||||
sdkGitSHA: "1234",
|
||||
session: .mock,
|
||||
appHooks: AppHooks())
|
||||
XCTAssertFalse(service.isEnabled)
|
||||
XCTAssertFalse(service.crashedLastRun)
|
||||
#expect(!service.isEnabled)
|
||||
#expect(!service.crashedLastRun)
|
||||
}
|
||||
|
||||
@MainActor func testSubmitBugReportWithRealService() async throws {
|
||||
@Test @MainActor
|
||||
func submitBugReportWithRealService() async throws {
|
||||
let urlPublisher: CurrentValueSubject<RageshakeConfiguration, Never> = .init(.url("https://example.com/submit"))
|
||||
let service = BugReportService(rageshakeURLPublisher: urlPublisher.asCurrentValuePublisher(),
|
||||
applicationID: "mock_app_id",
|
||||
sdkGitSHA: "1234",
|
||||
session: .mock,
|
||||
appHooks: AppHooks())
|
||||
|
||||
|
||||
let bugReport = BugReport(userID: "@mock:client.com",
|
||||
deviceID: nil,
|
||||
ed25519: nil,
|
||||
@@ -92,12 +98,14 @@ class BugReportServiceTests: XCTestCase {
|
||||
let progressSubject = CurrentValueSubject<Double, Never>(0.0)
|
||||
let response = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
|
||||
|
||||
XCTAssertEqual(response.reportURL, "https://example.com/123")
|
||||
#expect(response.reportURL == "https://example.com/123")
|
||||
}
|
||||
|
||||
@MainActor func testConfigurations() async throws {
|
||||
@Test
|
||||
@MainActor
|
||||
func configurations() async throws {
|
||||
guard case let .url(initialURL) = appSettings.bugReportRageshakeURL.publisher.value else {
|
||||
XCTFail("Unexpected initial configuration.")
|
||||
Issue.record("Unexpected initial configuration.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,14 +114,14 @@ class BugReportServiceTests: XCTestCase {
|
||||
sdkGitSHA: "1234",
|
||||
session: .mock,
|
||||
appHooks: AppHooks())
|
||||
XCTAssertTrue(service.isEnabled)
|
||||
#expect(service.isEnabled)
|
||||
|
||||
appSettings.bugReportRageshakeURL.applyRemoteValue(.disabled)
|
||||
XCTAssertFalse(service.isEnabled)
|
||||
#expect(!service.isEnabled)
|
||||
|
||||
appSettings.bugReportRageshakeURL.applyRemoteValue(.url("https://bugs.server.net/submit"))
|
||||
XCTAssertTrue(service.isEnabled)
|
||||
|
||||
#expect(service.isEnabled)
|
||||
|
||||
let bugReport = BugReport(userID: "@mock:client.com",
|
||||
deviceID: nil,
|
||||
ed25519: nil,
|
||||
@@ -126,14 +134,14 @@ class BugReportServiceTests: XCTestCase {
|
||||
let progressSubject = CurrentValueSubject<Double, Never>(0.0)
|
||||
let customConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
|
||||
|
||||
XCTAssertEqual(customConfigurationResponse.reportURL, "https://bugs.server.net/123")
|
||||
#expect(customConfigurationResponse.reportURL == "https://bugs.server.net/123")
|
||||
|
||||
appSettings.bugReportRageshakeURL.reset()
|
||||
XCTAssertTrue(service.isEnabled)
|
||||
#expect(service.isEnabled)
|
||||
|
||||
let defaultConfigurationResponse = try await service.submitBugReport(bugReport, progressListener: progressSubject).get()
|
||||
|
||||
XCTAssertEqual(defaultConfigurationResponse.reportURL, initialURL.absoluteString.replacingOccurrences(of: "submit", with: "123"))
|
||||
#expect(defaultConfigurationResponse.reportURL == initialURL.absoluteString.replacingOccurrences(of: "submit", with: "123"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,15 +158,15 @@ private class MockURLProtocol: URLProtocol {
|
||||
client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func stopLoading() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class CallScreenViewModelTests: XCTestCase { }
|
||||
@@ -24,7 +24,7 @@ class ChatsTabFlowCoordinatorTests: XCTestCase {
|
||||
var detailCoordinator: CoordinatorProtocol? {
|
||||
splitCoordinator?.detailCoordinator
|
||||
}
|
||||
|
||||
|
||||
var detailNavigationStack: NavigationStackCoordinator? {
|
||||
detailCoordinator as? NavigationStackCoordinator
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
}
|
||||
@@ -99,7 +99,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
private var viewModel: ComposerToolbarViewModel!
|
||||
private var completionSuggestionServiceMock: CompletionSuggestionServiceMock!
|
||||
private var draftServiceMock: ComposerDraftServiceMock!
|
||||
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
@@ -30,14 +30,14 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
override func tearDown() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
|
||||
func testComposerFocus() {
|
||||
viewModel.process(timelineAction: .setMode(mode: .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)))
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
viewModel.process(timelineAction: .removeFocus)
|
||||
XCTAssertFalse(viewModel.state.bindings.composerFocused)
|
||||
}
|
||||
|
||||
|
||||
func testComposerMode() {
|
||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||
@@ -45,7 +45,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.process(timelineAction: .clear)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
}
|
||||
|
||||
|
||||
func testComposerModeIsPublished() {
|
||||
let mode: ComposerMode = .edit(originalEventOrTransactionID: .eventID("mock"), type: .default)
|
||||
let expectation = expectation(description: "Composer mode is published")
|
||||
@@ -59,22 +59,22 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(composerMode, mode)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
|
||||
viewModel.process(timelineAction: .setMode(mode: mode))
|
||||
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
|
||||
func testHandleKeyCommand() {
|
||||
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
||||
}
|
||||
|
||||
|
||||
func testComposerFocusAfterEnablingRTE() {
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
}
|
||||
|
||||
|
||||
func testRTEEnabledAfterSendingMessage() {
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFocused)
|
||||
@@ -82,7 +82,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.process(viewAction: .sendMessage)
|
||||
XCTAssertTrue(viewModel.state.bindings.composerFormattingEnabled)
|
||||
}
|
||||
|
||||
|
||||
func testAlertIsShownAfterLinkAction() {
|
||||
XCTAssertNil(viewModel.state.bindings.alertInfo)
|
||||
viewModel.process(viewAction: .enableTextFormatting)
|
||||
@@ -139,7 +139,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||
|
||||
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
waveform: .data(waveformData),
|
||||
isUploading: false)))
|
||||
viewModel.saveDraft()
|
||||
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(draftServiceMock.saveDraftCalled)
|
||||
XCTAssertEqual(draftServiceMock.clearDraftCallsCount, 1)
|
||||
@@ -588,7 +588,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
let text = "Hello @room"
|
||||
viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil))
|
||||
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
switch action {
|
||||
case let .sendMessage(plainText, _, _, intentionalMentions):
|
||||
@@ -673,7 +673,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
roomProxyMock.getMemberUserIDClosure = { _ in
|
||||
.success(roomMemberProxyMock)
|
||||
}
|
||||
|
||||
|
||||
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
|
||||
roomProxyMock.underlyingIdentityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
|
||||
|
||||
@@ -712,7 +712,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
return .failure(.sdkError(ClientProxyMockError.generic))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// There are 2 violations, ensure that resolving the first one is not enough
|
||||
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([
|
||||
IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation),
|
||||
|
||||
@@ -7,55 +7,61 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
// swiftlint:disable force_unwrapping
|
||||
|
||||
class DateTests: XCTestCase {
|
||||
@Suite
|
||||
struct DateTests {
|
||||
let calendar = Calendar.current
|
||||
let startOfToday = Calendar.current.startOfDay(for: .now)
|
||||
let startOfYesterday = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: -1, to: .now)!)
|
||||
|
||||
func testMinimalDateFormatting() throws {
|
||||
let today = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday))
|
||||
XCTAssertEqual(today.formattedMinimal(), today.formatted(date: .omitted, time: .shortened))
|
||||
|
||||
let yesterday = try XCTUnwrap(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
||||
XCTAssertEqual(yesterday.formattedMinimal(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
|
||||
let nearYesterday = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
||||
XCTAssertEqual(nearYesterday.formattedMinimal(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
|
||||
let threeDaysAgo = try XCTUnwrap(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
||||
XCTAssertEqual(threeDaysAgo.formattedMinimal(), threeDaysAgo.formatted(.dateTime.weekday(.wide)))
|
||||
|
||||
let sometimeInTheLastYear = try XCTUnwrap(calendar.date(byAdding: .month, value: -10, to: startOfToday))
|
||||
XCTAssertEqual(sometimeInTheLastYear.formattedMinimal(), sometimeInTheLastYear.formatted(.dateTime.day().month()))
|
||||
|
||||
let theMillennium = try XCTUnwrap(calendar.date(from: DateComponents(year: 2000, month: 1, day: 1)))
|
||||
XCTAssertEqual(theMillennium.formattedMinimal(), theMillennium.formatted(.dateTime.year().day().month()))
|
||||
var startOfToday: Date {
|
||||
Calendar.current.startOfDay(for: .now)
|
||||
}
|
||||
|
||||
func testDateSeparatorFormatting() throws {
|
||||
let today = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday))
|
||||
XCTAssertEqual(today.formattedDateSeparator(), "Today")
|
||||
var startOfYesterday: Date {
|
||||
// swiftlint: disable:next force_unwrapping
|
||||
Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: -1, to: .now)!)
|
||||
}
|
||||
|
||||
@Test
|
||||
func minimalDateFormatting() throws {
|
||||
let today = try #require(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday))
|
||||
#expect(today.formattedMinimal() == today.formatted(date: .omitted, time: .shortened))
|
||||
|
||||
let yesterday = try XCTUnwrap(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
||||
XCTAssertEqual(yesterday.formattedDateSeparator(), "Yesterday")
|
||||
let yesterday = try #require(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
||||
#expect(yesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
|
||||
let nearYesterday = try XCTUnwrap(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
||||
XCTAssertEqual(nearYesterday.formattedDateSeparator(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
let nearYesterday = try #require(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
||||
#expect(nearYesterday.formattedMinimal() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
|
||||
let threeDaysAgo = try XCTUnwrap(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
||||
XCTAssertEqual(threeDaysAgo.formattedDateSeparator(), threeDaysAgo.formatted(.dateTime.weekday(.wide)))
|
||||
let threeDaysAgo = try #require(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
||||
#expect(threeDaysAgo.formattedMinimal() == threeDaysAgo.formatted(.dateTime.weekday(.wide)))
|
||||
|
||||
let sometimeInTheLastYear = try #require(calendar.date(byAdding: .month, value: -10, to: startOfToday))
|
||||
#expect(sometimeInTheLastYear.formattedMinimal() == sometimeInTheLastYear.formatted(.dateTime.day().month()))
|
||||
|
||||
let theMillennium = try #require(calendar.date(from: DateComponents(year: 2000, month: 1, day: 1)))
|
||||
#expect(theMillennium.formattedMinimal() == theMillennium.formatted(.dateTime.year().day().month()))
|
||||
}
|
||||
|
||||
@Test
|
||||
func dateSeparatorFormatting() throws {
|
||||
let today = try #require(calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday))
|
||||
#expect(today.formattedDateSeparator() == "Today")
|
||||
|
||||
let yesterday = try #require(calendar.date(byAdding: .hour, value: 1, to: startOfYesterday))
|
||||
#expect(yesterday.formattedDateSeparator() == "Yesterday")
|
||||
|
||||
let nearYesterday = try #require(calendar.date(byAdding: DateComponents(hour: -10), to: today))
|
||||
#expect(nearYesterday.formattedDateSeparator() == yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)))
|
||||
|
||||
let threeDaysAgo = try #require(calendar.date(byAdding: .day, value: -3, to: startOfToday))
|
||||
#expect(threeDaysAgo.formattedDateSeparator() == threeDaysAgo.formatted(.dateTime.weekday(.wide)))
|
||||
|
||||
// This test will fail during the first 6 days of the year.
|
||||
let startOfTheYear = try XCTUnwrap(calendar.dateInterval(of: .year, for: startOfToday)?.start)
|
||||
XCTAssertEqual(startOfTheYear.formattedDateSeparator(), startOfTheYear.formatted(.dateTime.weekday(.wide).day().month(.wide)))
|
||||
let startOfTheYear = try #require(calendar.dateInterval(of: .year, for: startOfToday)?.start)
|
||||
#expect(startOfTheYear.formattedDateSeparator() == startOfTheYear.formatted(.dateTime.weekday(.wide).day().month(.wide)))
|
||||
|
||||
let theMillennium = try XCTUnwrap(calendar.date(from: DateComponents(year: 2000, month: 1, day: 1)))
|
||||
XCTAssertEqual(theMillennium.formattedDateSeparator(), theMillennium.formatted(.dateTime.weekday(.wide).day().month(.wide).year()))
|
||||
let theMillennium = try #require(calendar.date(from: DateComponents(year: 2000, month: 1, day: 1)))
|
||||
#expect(theMillennium.formattedDateSeparator() == theMillennium.formatted(.dateTime.weekday(.wide).day().month(.wide).year()))
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable force_unwrapping
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class DeactivateAccountScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct DeactivateAccountScreenViewModelTests {
|
||||
var clientProxy: ClientProxyMock!
|
||||
var viewModel: DeactivateAccountScreenViewModelProtocol!
|
||||
|
||||
@@ -18,40 +20,34 @@ class DeactivateAccountScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
init() {
|
||||
clientProxy = ClientProxyMock(.init())
|
||||
viewModel = DeactivateAccountScreenViewModel(clientProxy: clientProxy, userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
func testDeactivate() async throws {
|
||||
@Test
|
||||
mutating func deactivate() async throws {
|
||||
try await validateDeactivate(erasingData: false)
|
||||
}
|
||||
|
||||
func testDeactivateAndErase() async throws {
|
||||
@Test
|
||||
mutating func deactivateAndErase() async throws {
|
||||
try await validateDeactivate(erasingData: true)
|
||||
}
|
||||
|
||||
func validateDeactivate(erasingData shouldErase: Bool) async throws {
|
||||
mutating func validateDeactivate(erasingData shouldErase: Bool) async throws {
|
||||
let enteredPassword = UUID().uuidString
|
||||
|
||||
clientProxy.deactivateAccountPasswordEraseDataClosure = { [weak self] password, eraseData in
|
||||
guard let self else { return .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||
clientProxy.deactivateAccountPasswordEraseDataClosure = { [weak clientProxy] password, eraseData in
|
||||
guard let clientProxy else { return .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||
|
||||
if clientProxy.deactivateAccountPasswordEraseDataCallsCount == 1 {
|
||||
if password != nil {
|
||||
XCTFail("The password shouldn't be sent first time round.")
|
||||
}
|
||||
if eraseData != shouldErase {
|
||||
XCTFail("The erase parameter is unexpected.")
|
||||
}
|
||||
#expect(password == nil, "The password shouldn't be sent first time round.")
|
||||
#expect(eraseData == shouldErase, "The erase parameter is unexpected.")
|
||||
return .failure(.sdkError(ClientProxyMockError.generic))
|
||||
} else {
|
||||
if password != enteredPassword {
|
||||
XCTFail("The password should match the user's input on the second call.")
|
||||
}
|
||||
if eraseData != shouldErase {
|
||||
XCTFail("The erase parameter is unexpected.")
|
||||
}
|
||||
#expect(password == enteredPassword, "The password should match the user's input on the second call.")
|
||||
#expect(eraseData == shouldErase, "The erase parameter is unexpected.")
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
@@ -59,23 +55,21 @@ class DeactivateAccountScreenViewModelTests: XCTestCase {
|
||||
context.eraseData = shouldErase
|
||||
context.password = enteredPassword
|
||||
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
let deferredState = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
context.send(viewAction: .deactivate)
|
||||
try await deferredState.fulfill()
|
||||
|
||||
guard let confirmationAction = context.alertInfo?.primaryButton.action else {
|
||||
XCTFail("Couldn't find the confirmation action.")
|
||||
return
|
||||
}
|
||||
let confirmationAction = try #require(context.alertInfo?.primaryButton.action,
|
||||
"Couldn't find the confirmation action.")
|
||||
|
||||
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .accountDeactivated }
|
||||
confirmationAction()
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataCallsCount, 2)
|
||||
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.password, enteredPassword)
|
||||
XCTAssertEqual(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.eraseData, shouldErase)
|
||||
#expect(clientProxy.deactivateAccountPasswordEraseDataCallsCount == 2)
|
||||
#expect(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.password == enteredPassword)
|
||||
#expect(clientProxy.deactivateAccountPasswordEraseDataReceivedArguments?.eraseData == shouldErase)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class DeclineAndBlockScreenViewModelTests: XCTestCase {
|
||||
var viewModel: DeclineAndBlockScreenViewModelProtocol!
|
||||
var clientProxy: ClientProxyMock!
|
||||
@Suite
|
||||
struct DeclineAndBlockScreenViewModelTests {
|
||||
var viewModel: DeclineAndBlockScreenViewModelProtocol
|
||||
var clientProxy: ClientProxyMock
|
||||
|
||||
var context: DeclineAndBlockScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
clientProxy = ClientProxyMock(.init())
|
||||
viewModel = DeclineAndBlockScreenViewModel(userID: "@alice:matrix.org",
|
||||
roomID: "!room:matrix.org",
|
||||
@@ -26,39 +27,42 @@ class DeclineAndBlockScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(context.viewState.isDeclineDisabled)
|
||||
XCTAssertFalse(context.shouldReport)
|
||||
XCTAssertTrue(context.shouldBlockUser)
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(!context.viewState.isDeclineDisabled)
|
||||
#expect(!context.shouldReport)
|
||||
#expect(context.shouldBlockUser)
|
||||
}
|
||||
|
||||
func testDeclineDisabled() {
|
||||
@Test
|
||||
mutating func declineDisabled() {
|
||||
context.shouldBlockUser = false
|
||||
XCTAssertTrue(context.viewState.isDeclineDisabled)
|
||||
XCTAssertFalse(context.shouldReport)
|
||||
XCTAssertFalse(context.shouldBlockUser)
|
||||
#expect(context.viewState.isDeclineDisabled)
|
||||
#expect(!context.shouldReport)
|
||||
#expect(!context.shouldBlockUser)
|
||||
context.shouldReport = true
|
||||
// Should report set to `true` always requires a non empty reason
|
||||
XCTAssertTrue(context.viewState.isDeclineDisabled)
|
||||
#expect(context.viewState.isDeclineDisabled)
|
||||
context.reportReason = "Test reason"
|
||||
XCTAssertFalse(context.viewState.isDeclineDisabled)
|
||||
#expect(!context.viewState.isDeclineDisabled)
|
||||
}
|
||||
|
||||
func testDeclineBlockAndReport() async throws {
|
||||
@Test
|
||||
mutating func declineBlockAndReport() async throws {
|
||||
let reason = "Test reason"
|
||||
clientProxy.roomForIdentifierClosure = { id in
|
||||
XCTAssertEqual(id, "!room:matrix.org")
|
||||
#expect(id == "!room:matrix.org")
|
||||
let roomProxyMock = InvitedRoomProxyMock(.init(id: id))
|
||||
roomProxyMock.rejectInvitationReturnValue = .success(())
|
||||
return .invited(InvitedRoomProxyMock(.init(id: id)))
|
||||
}
|
||||
clientProxy.reportRoomForIdentifierReasonClosure = { id, reasonValue in
|
||||
XCTAssertEqual(id, "!room:matrix.org")
|
||||
XCTAssertEqual(reasonValue, reason)
|
||||
#expect(id == "!room:matrix.org")
|
||||
#expect(reasonValue == reason)
|
||||
return .success(())
|
||||
}
|
||||
clientProxy.ignoreUserClosure = { userId in
|
||||
XCTAssertEqual(userId, "@alice:matrix.org")
|
||||
#expect(userId == "@alice:matrix.org")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
@@ -70,8 +74,8 @@ class DeclineAndBlockScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
context.send(viewAction: .decline)
|
||||
try await deferredAction.fulfill()
|
||||
XCTAssertTrue(clientProxy.roomForIdentifierCalled)
|
||||
XCTAssertTrue(clientProxy.reportRoomForIdentifierReasonCalled)
|
||||
XCTAssertTrue(clientProxy.ignoreUserCalled)
|
||||
#expect(clientProxy.roomForIdentifierCalled)
|
||||
#expect(clientProxy.reportRoomForIdentifierReasonCalled)
|
||||
#expect(clientProxy.ignoreUserCalled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Observation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class DeferredFulfillmentTests: XCTestCase {
|
||||
@Suite
|
||||
struct DeferredFulfillmentTests {
|
||||
private let observable = SomeObservable()
|
||||
|
||||
func testObservableWithoutUpdate() async throws {
|
||||
@Test
|
||||
func observableWithoutUpdate() async throws {
|
||||
// Given a deferred fulfilment on a value that already matches the expected value.
|
||||
let initialValue = observable.counter
|
||||
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == initialValue }
|
||||
@@ -22,35 +25,38 @@ class DeferredFulfillmentTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testObservableWithSynchronousUpdate() async throws {
|
||||
@Test
|
||||
func observableWithSynchronousUpdate() async throws {
|
||||
// Given a deferred fulfilment for an expected value.
|
||||
let newValue = 100
|
||||
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue }
|
||||
|
||||
// When that value is changed synchronously.
|
||||
observable.counter = newValue
|
||||
XCTAssertEqual(observable.counter, newValue)
|
||||
#expect(observable.counter == newValue)
|
||||
|
||||
// Then the test should be fulfilled.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(observable.counter, newValue)
|
||||
#expect(observable.counter == newValue)
|
||||
}
|
||||
|
||||
func testObservableAsynchronousUpdate() async throws {
|
||||
@Test
|
||||
func observableAsynchronousUpdate() async throws {
|
||||
// Given a deferred fulfilment for an expected value.
|
||||
let newValue = 100
|
||||
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == newValue }
|
||||
|
||||
// When that value is changed asynchronously.
|
||||
Task { try await observable.setCounter(newValue, delay: .seconds(1)) }
|
||||
XCTAssertEqual(observable.counter, 0)
|
||||
#expect(observable.counter == 0)
|
||||
|
||||
// Then the test should be fulfilled once the update has taken place.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(observable.counter, newValue)
|
||||
#expect(observable.counter == newValue)
|
||||
}
|
||||
|
||||
func testObservableMultipleUpdates() async throws {
|
||||
@Test
|
||||
func observableMultipleUpdates() async throws {
|
||||
// Given a deferred fulfilment for an expected value.
|
||||
let finalValue = 500
|
||||
let deferred = deferFulfillment(observable.observe(\.counter)) { $0 == finalValue }
|
||||
@@ -61,11 +67,11 @@ class DeferredFulfillmentTests: XCTestCase {
|
||||
try await observable.setCounter(250, delay: .seconds(.random(in: 1.0...2.0)))
|
||||
try await observable.setCounter(finalValue, delay: .seconds(.random(in: 1.0...2.0)))
|
||||
}
|
||||
XCTAssertEqual(observable.counter, 0)
|
||||
#expect(observable.counter == 0)
|
||||
|
||||
// Then the test should be fulfilled once the expected update has taken place.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(observable.counter, finalValue)
|
||||
#expect(observable.counter == finalValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class DeveloperOptionsScreenViewModelTests: XCTestCase { }
|
||||
@@ -76,7 +76,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
|
||||
XCTAssertNil(roomProxy.infoPublisher.value.canonicalAlias)
|
||||
XCTAssertEqual(viewModel.context.viewState.bindings.desiredAliasLocalPart, "room-name")
|
||||
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
@@ -107,7 +107,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
context.desiredAliasLocalPart = "room-name"
|
||||
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
@@ -144,7 +144,7 @@ class EditRoomAddressScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
context.desiredAliasLocalPart = "room-name"
|
||||
|
||||
|
||||
let publishingExpectation = expectation(description: "Wait for publishing")
|
||||
roomProxy.publishRoomAliasInRoomDirectoryClosure = { roomAlias in
|
||||
defer { publishingExpectation.fulfill() }
|
||||
|
||||
@@ -8,75 +8,81 @@
|
||||
import Clocks
|
||||
@testable import ElementX
|
||||
import PushKit
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ElementCallServiceTests: XCTestCase {
|
||||
var callProvider: CXProviderMock!
|
||||
var currentDate: Date!
|
||||
var testClock: TestClock<Duration>!
|
||||
var pushRegistry: PKPushRegistry!
|
||||
@Suite
|
||||
final class ElementCallServiceTests {
|
||||
private var callProvider: CXProviderMock!
|
||||
private var currentDate: Date!
|
||||
private var testClock: TestClock<Duration>!
|
||||
private var pushRegistry: PKPushRegistry!
|
||||
private var service: ElementCallService!
|
||||
|
||||
var service: ElementCallService!
|
||||
init() {
|
||||
pushRegistry = PKPushRegistry(queue: nil)
|
||||
callProvider = CXProviderMock(.init())
|
||||
currentDate = Date()
|
||||
testClock = TestClock()
|
||||
let dateProvider: () -> Date = {
|
||||
self.currentDate
|
||||
}
|
||||
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: testClock, now: dateProvider))
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
callProvider = nil
|
||||
currentDate = nil
|
||||
testClock = nil
|
||||
pushRegistry = nil
|
||||
}
|
||||
|
||||
func testIncomingCall() async {
|
||||
setupService()
|
||||
@Test
|
||||
func incomingCall() async {
|
||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Call accepted")
|
||||
|
||||
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
|
||||
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
XCTAssertTrue(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
}
|
||||
|
||||
func disabled_testCallIsTimingOut() async {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
let expectation = XCTestExpectation(description: "Call accepted")
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
let expectation2 = XCTestExpectation(description: "Call ended unanswered")
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
expectation2.fulfill()
|
||||
} else {
|
||||
XCTFail("Call should have ended as unanswered")
|
||||
await confirmation { confirmation in
|
||||
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 30)
|
||||
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
|
||||
// advance past the timeout
|
||||
await testClock.advance(by: .seconds(30))
|
||||
await fulfillment(of: [expectation2], timeout: 1)
|
||||
#expect(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
}
|
||||
|
||||
func testExpiredRingLifetimeIsIgnored() {
|
||||
setupService()
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
@Test
|
||||
func callIsTimingOut() async {
|
||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
await confirmation { confirmation in
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) {
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
|
||||
await confirmation { confirmation in
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
confirmation()
|
||||
} else {
|
||||
Issue.record("Call should have ended as unanswered")
|
||||
}
|
||||
}
|
||||
|
||||
// advance past the timeout
|
||||
await testClock.advance(by: .seconds(30))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func expiredRingLifetimeIsIgnored() {
|
||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 20)
|
||||
|
||||
@@ -87,45 +93,31 @@ class ElementCallServiceTests: XCTestCase {
|
||||
for: .voIP) { }
|
||||
sleep(20)
|
||||
|
||||
XCTAssertTrue(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
}
|
||||
|
||||
func disabled_testLifetimeIsCapped() async throws {
|
||||
setupService()
|
||||
|
||||
let expectation = expectation(description: "Call has ended unanswered")
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
XCTFail("Call should have ended as unanswered")
|
||||
@Test
|
||||
func lifetimeIsCapped() async {
|
||||
await confirmation { confirmation in
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
confirmation()
|
||||
} else {
|
||||
Issue.record("Call should have ended as unanswered")
|
||||
}
|
||||
}
|
||||
|
||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) { }
|
||||
|
||||
// Advance past the max timeout but below the 300
|
||||
await testClock.advance(by: .seconds(100))
|
||||
}
|
||||
|
||||
XCTAssertFalse(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||
|
||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
|
||||
|
||||
service.pushRegistry(pushRegistry,
|
||||
didReceiveIncomingPushWith: pushPayload,
|
||||
for: .voIP) { }
|
||||
|
||||
// Advance past the max timeout but below the 300
|
||||
await testClock.advance(by: .seconds(100))
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupService() {
|
||||
pushRegistry = PKPushRegistry(queue: nil)
|
||||
callProvider = CXProviderMock(.init())
|
||||
currentDate = Date()
|
||||
testClock = TestClock()
|
||||
let dateProvider: () -> Date = {
|
||||
self.currentDate
|
||||
}
|
||||
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: testClock, now: dateProvider))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
final class EmojiPickerScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct EmojiPickerScreenViewModelTests {
|
||||
var timelineProxy: TimelineProxyMock!
|
||||
|
||||
var viewModel: EmojiPickerScreenViewModel!
|
||||
@@ -18,25 +19,38 @@ final class EmojiPickerScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testToggleReaction() async throws {
|
||||
@Test
|
||||
mutating func toggleReaction() async throws {
|
||||
setupViewModel()
|
||||
let reaction = "👋"
|
||||
|
||||
let expectation = XCTestExpectation(description: "Toggle reaction")
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
timelineProxy.toggleReactionToClosure = { toggledReaction, _ in
|
||||
XCTAssertEqual(toggledReaction, reaction)
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
try await confirmation { confirmation in
|
||||
var toggleReactionCalled = false
|
||||
timelineProxy.toggleReactionToClosure = { toggledReaction, _ in
|
||||
defer {
|
||||
confirmation()
|
||||
toggleReactionCalled = true
|
||||
}
|
||||
#expect(toggledReaction == reaction)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
context.send(viewAction: .emojiTapped(emoji: .init(id: "wave", value: reaction)))
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Since the reaction is called asynchronously after dismissing the picker
|
||||
// We need to actively wait for the function to be called before fulfilling the test.
|
||||
while !toggleReactionCalled {
|
||||
await Task.yield()
|
||||
}
|
||||
}
|
||||
context.send(viewAction: .emojiTapped(emoji: .init(id: "wave", value: reaction)))
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(selectedEmojis: Set<String> = []) {
|
||||
private mutating func setupViewModel(selectedEmojis: Set<String> = []) {
|
||||
timelineProxy = TimelineProxyMock(.init())
|
||||
|
||||
viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
final class EmojiProviderTests: XCTestCase {
|
||||
func testWhenEmojisLoadedCategoriesAreLoadedFromLoader() async {
|
||||
@Suite
|
||||
struct EmojiProviderTests {
|
||||
@Test @MainActor
|
||||
func emojisLoadedCategoriesAreLoadedFromLoader() async {
|
||||
let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"])
|
||||
let category = EmojiCategory(id: "test", emojis: [item])
|
||||
|
||||
@@ -21,10 +23,11 @@ final class EmojiProviderTests: XCTestCase {
|
||||
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
let categories = await emojiProvider.categories()
|
||||
XCTAssertEqual(emojiLoaderMock.categories, categories)
|
||||
#expect(emojiLoaderMock.categories == categories)
|
||||
}
|
||||
|
||||
func testWhenEmojisLoadedAndSearchStringEmptyAllCategoriesReturned() async {
|
||||
@Test @MainActor
|
||||
func emojisLoadedAndSearchStringEmptyAllCategoriesReturned() async {
|
||||
let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"])
|
||||
let category = EmojiCategory(id: "test", emojis: [item])
|
||||
|
||||
@@ -34,10 +37,11 @@ final class EmojiProviderTests: XCTestCase {
|
||||
let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
|
||||
|
||||
let categories = await emojiProvider.categories(searchString: "")
|
||||
XCTAssertEqual(emojiLoaderMock.categories, categories)
|
||||
#expect(emojiLoaderMock.categories == categories)
|
||||
}
|
||||
|
||||
func testWhenEmojisLoadedSecondTimeCachedValuesAreUsed() async {
|
||||
@Test @MainActor
|
||||
func emojisLoadedSecondTimeCachedValuesAreUsed() async {
|
||||
let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"])
|
||||
let item2 = EmojiItem(label: "test2", unicode: "test2", keywords: ["3", "4"], shortcodes: ["3", "4"])
|
||||
let categoriesForFirstLoad = [EmojiCategory(id: "test",
|
||||
@@ -54,10 +58,11 @@ final class EmojiProviderTests: XCTestCase {
|
||||
emojiLoaderMock.categories = categoriesForSecondLoad
|
||||
|
||||
let categories = await emojiProvider.categories()
|
||||
XCTAssertEqual(categories, categoriesForFirstLoad)
|
||||
#expect(categories == categoriesForFirstLoad)
|
||||
}
|
||||
|
||||
func testWhenEmojisSearchedCorrectNumberOfCategoriesReturned() async {
|
||||
@Test @MainActor
|
||||
func emojisSearchedCorrectNumberOfCategoriesReturned() async {
|
||||
let searchString = "smile"
|
||||
var categories = [EmojiCategory]()
|
||||
let item0WithSearchString = EmojiItem(label: "emoji0", unicode: "\(searchString)_123", keywords: ["key1", "key1"], shortcodes: ["key1", "key1"])
|
||||
@@ -82,8 +87,8 @@ final class EmojiProviderTests: XCTestCase {
|
||||
|
||||
_ = await emojiProvider.categories()
|
||||
let result = await emojiProvider.categories(searchString: searchString)
|
||||
XCTAssertEqual(result.count, 2)
|
||||
XCTAssertEqual(result.first?.emojis.count, 4)
|
||||
#expect(result.count == 2)
|
||||
#expect(result.first?.emojis.count == 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,24 +8,27 @@
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class ExpiringTaskRunnerTests: XCTestCase {
|
||||
@Suite
|
||||
struct ExpiringTaskRunnerTests {
|
||||
enum ExpiringTaskTestError: Error {
|
||||
case failed
|
||||
}
|
||||
|
||||
func testSuccedingTask() async {
|
||||
@Test
|
||||
func succedingTask() async throws {
|
||||
let runner = ExpiringTaskRunner {
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
return true
|
||||
}
|
||||
|
||||
let result = try? await runner.run(timeout: .seconds(1))
|
||||
XCTAssertEqual(result, true)
|
||||
let result = try await runner.run(timeout: .seconds(1))
|
||||
#expect(result == true)
|
||||
}
|
||||
|
||||
func testFailingTask() async {
|
||||
@Test
|
||||
func failingTask() async {
|
||||
let runner: ExpiringTaskRunner<Result<String, ExpiringTaskTestError>> = ExpiringTaskRunner {
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
return .failure(.failed)
|
||||
@@ -34,20 +37,21 @@ class ExpiringTaskRunnerTests: XCTestCase {
|
||||
do {
|
||||
_ = try await runner.run(timeout: .seconds(1))
|
||||
} catch {
|
||||
XCTAssertEqual(error as? ExpiringTaskTestError, ExpiringTaskTestError.failed)
|
||||
#expect(error as? ExpiringTaskTestError == ExpiringTaskTestError.failed)
|
||||
}
|
||||
}
|
||||
|
||||
func testTimeoutTask() async {
|
||||
@Test
|
||||
func timeoutTask() async {
|
||||
let runner = ExpiringTaskRunner {
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
_ = try await runner.run(timeout: .milliseconds(100))
|
||||
} catch {
|
||||
XCTAssertEqual(error as? ExpiringTaskRunnerError, ExpiringTaskRunnerError.timeout)
|
||||
#expect(error as? ExpiringTaskRunnerError == ExpiringTaskRunnerError.timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,76 +7,87 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class GeoURITests: XCTestCase {
|
||||
func testValidPositiveCoordinates() throws {
|
||||
@Suite
|
||||
struct GeoURITests {
|
||||
@Test
|
||||
func validPositiveCoordinates() throws {
|
||||
let string = "geo:53.9980310155285,8.25347900390625;u=10.123"
|
||||
let uri = try XCTUnwrap(GeoURI(string: string))
|
||||
XCTAssertEqual(uri.latitude, 53.9980310155285)
|
||||
XCTAssertEqual(uri.longitude, 8.25347900390625)
|
||||
XCTAssertEqual(uri.uncertainty, 10.123)
|
||||
XCTAssertEqual(uri.string, string)
|
||||
let uri = try #require(GeoURI(string: string))
|
||||
#expect(uri.latitude == 53.9980310155285)
|
||||
#expect(uri.longitude == 8.25347900390625)
|
||||
#expect(uri.uncertainty == 10.123)
|
||||
#expect(uri.string == string)
|
||||
}
|
||||
|
||||
func testValidNegativeCoordinates() throws {
|
||||
|
||||
@Test
|
||||
func validNegativeCoordinates() throws {
|
||||
let string = "geo:-53.9980310155285,-8.25347900390625;u=10"
|
||||
let uri = try XCTUnwrap(GeoURI(string: string))
|
||||
XCTAssertEqual(uri.latitude, -53.9980310155285)
|
||||
XCTAssertEqual(uri.longitude, -8.25347900390625)
|
||||
XCTAssertEqual(uri.uncertainty, 10)
|
||||
XCTAssertEqual(uri.string, string)
|
||||
let uri = try #require(GeoURI(string: string))
|
||||
#expect(uri.latitude == -53.9980310155285)
|
||||
#expect(uri.longitude == -8.25347900390625)
|
||||
#expect(uri.uncertainty == 10)
|
||||
#expect(uri.string == string)
|
||||
}
|
||||
|
||||
func testValidMixedCoordinates() throws {
|
||||
|
||||
@Test
|
||||
func validMixedCoordinates() throws {
|
||||
let string = "geo:53.9980310155285,-8.25347900390625;u=10"
|
||||
let uri = try XCTUnwrap(GeoURI(string: string))
|
||||
XCTAssertEqual(uri.latitude, 53.9980310155285)
|
||||
XCTAssertEqual(uri.longitude, -8.25347900390625)
|
||||
XCTAssertEqual(uri.uncertainty, 10)
|
||||
XCTAssertEqual(uri.string, string)
|
||||
let uri = try #require(GeoURI(string: string))
|
||||
#expect(uri.latitude == 53.9980310155285)
|
||||
#expect(uri.longitude == -8.25347900390625)
|
||||
#expect(uri.uncertainty == 10)
|
||||
#expect(uri.string == string)
|
||||
}
|
||||
|
||||
func testValidCoordinatesNoUncertainty() throws {
|
||||
|
||||
@Test
|
||||
func validCoordinatesNoUncertainty() throws {
|
||||
let string = "geo:53.9980310155285,-8.25347900390625"
|
||||
let uri = try XCTUnwrap(GeoURI(string: string))
|
||||
XCTAssertEqual(uri.latitude, 53.9980310155285)
|
||||
XCTAssertEqual(uri.longitude, -8.25347900390625)
|
||||
XCTAssertNil(uri.uncertainty)
|
||||
XCTAssertEqual(uri.string, string)
|
||||
let uri = try #require(GeoURI(string: string))
|
||||
#expect(uri.latitude == 53.9980310155285)
|
||||
#expect(uri.longitude == -8.25347900390625)
|
||||
#expect(uri.uncertainty == nil)
|
||||
#expect(uri.string == string)
|
||||
}
|
||||
|
||||
func testValidIntegerCoordinates() throws {
|
||||
|
||||
@Test
|
||||
func validIntegerCoordinates() throws {
|
||||
let string = "geo:53,-8;u=35"
|
||||
let uri = try XCTUnwrap(GeoURI(string: string))
|
||||
XCTAssertEqual(uri.latitude, 53)
|
||||
XCTAssertEqual(uri.longitude, -8)
|
||||
XCTAssertEqual(uri.uncertainty, 35)
|
||||
XCTAssertEqual(uri.string, "geo:53,-8;u=35")
|
||||
let uri = try #require(GeoURI(string: string))
|
||||
#expect(uri.latitude == 53)
|
||||
#expect(uri.longitude == -8)
|
||||
#expect(uri.uncertainty == 35)
|
||||
#expect(uri.string == "geo:53,-8;u=35")
|
||||
}
|
||||
|
||||
func testFormattingExponentialNotation() {
|
||||
|
||||
@Test
|
||||
func formattingExponentialNotation() {
|
||||
let uri = GeoURI(latitude: 1e2, longitude: -1e-2, uncertainty: 1e-4)
|
||||
XCTAssertEqual(uri.string, "geo:100,-0.01;u=0.0001")
|
||||
#expect(uri.string == "geo:100,-0.01;u=0.0001")
|
||||
}
|
||||
|
||||
func testInvalidURI1() {
|
||||
|
||||
@Test
|
||||
func invalidURI1() {
|
||||
let string = "geo:53.99803101552848,-8.25347900390625;" // final ; without a u=number
|
||||
XCTAssertNil(GeoURI(string: string))
|
||||
#expect(GeoURI(string: string) == nil)
|
||||
}
|
||||
|
||||
func testInvalidURI2() {
|
||||
|
||||
@Test
|
||||
func invalidURI2() {
|
||||
let string = "geo:53.99803101552848, -8.25347900390625;" // spaces in the middle
|
||||
XCTAssertNil(GeoURI(string: string))
|
||||
#expect(GeoURI(string: string) == nil)
|
||||
}
|
||||
|
||||
func testInvalidURI3() {
|
||||
|
||||
@Test
|
||||
func invalidURI3() {
|
||||
let string = "geo:+53.99803101552848,-8.25347900390625" // '+' before a number
|
||||
XCTAssertNil(GeoURI(string: string))
|
||||
#expect(GeoURI(string: string) == nil)
|
||||
}
|
||||
|
||||
func testInvalidURI4() {
|
||||
|
||||
@Test
|
||||
func invalidURI4() {
|
||||
let string = "geo:53.99803101552848,-8.25347900390625;u=-20" // u is negative
|
||||
XCTAssertNil(GeoURI(string: string))
|
||||
#expect(GeoURI(string: string) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,48 +8,44 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class GlobalSearchScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct GlobalSearchScreenViewModelTests {
|
||||
var viewModel: GlobalSearchScreenViewModelProtocol!
|
||||
var context: GlobalSearchScreenViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
cancellables.removeAll()
|
||||
init() {
|
||||
viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testSearching() async throws {
|
||||
let defered = deferFulfillment(context.$viewState) { state in
|
||||
|
||||
@Test
|
||||
mutating func searching() async throws {
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.rooms.count == 1
|
||||
}
|
||||
|
||||
context.searchQuery = "Second"
|
||||
|
||||
try await defered.fulfill()
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSelection() {
|
||||
let expectation = expectation(description: "Wait for confirmation")
|
||||
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .select(let roomID):
|
||||
XCTAssertEqual(roomID, "2")
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@Test
|
||||
func roomSelection() async throws {
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
switch action {
|
||||
case .select(let roomID):
|
||||
return roomID == "2"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
context.send(viewAction: .select(roomID: "2"))
|
||||
|
||||
waitForExpectations(timeout: 5.0)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,19 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class HomeScreenRoomTests: XCTestCase {
|
||||
@Suite
|
||||
struct HomeScreenRoomTests {
|
||||
var roomSummary: RoomSummary!
|
||||
|
||||
func setupRoomSummary(isMarkedUnread: Bool,
|
||||
unreadMessagesCount: UInt,
|
||||
unreadMentionsCount: UInt,
|
||||
unreadNotificationsCount: UInt,
|
||||
notificationMode: RoomNotificationModeProxy,
|
||||
hasOngoingCall: Bool) {
|
||||
mutating func setupRoomSummary(isMarkedUnread: Bool,
|
||||
unreadMessagesCount: UInt,
|
||||
unreadMentionsCount: UInt,
|
||||
unreadNotificationsCount: UInt,
|
||||
notificationMode: RoomNotificationModeProxy,
|
||||
hasOngoingCall: Bool) {
|
||||
roomSummary = RoomSummary(room: .init(noHandle: .init()),
|
||||
id: "Test room",
|
||||
joinRequestType: nil,
|
||||
@@ -44,7 +45,8 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
isTombstoned: false)
|
||||
}
|
||||
|
||||
func testNoBadge() {
|
||||
@Test
|
||||
mutating func noBadge() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -54,14 +56,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertFalse(room.isHighlighted)
|
||||
XCTAssertFalse(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(!room.isHighlighted)
|
||||
#expect(!room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testAllBadgesExceptMute() {
|
||||
@Test
|
||||
mutating func allBadgesExceptMute() {
|
||||
setupRoomSummary(isMarkedUnread: true,
|
||||
unreadMessagesCount: 5,
|
||||
unreadMentionsCount: 5,
|
||||
@@ -71,14 +74,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertTrue(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertTrue(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testUnhighlightedDot() {
|
||||
@Test
|
||||
mutating func unhighlightedDot() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 5,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -88,14 +92,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertFalse(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(!room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testHighlightedDot() {
|
||||
@Test
|
||||
mutating func highlightedDot() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -105,14 +110,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testHighlightedMentionAndDot() {
|
||||
@Test
|
||||
mutating func highlightedMentionAndDot() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 5,
|
||||
@@ -122,14 +128,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertTrue(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testUnhighlightedCall() {
|
||||
@Test
|
||||
mutating func unhighlightedCall() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -139,14 +146,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertFalse(room.isHighlighted)
|
||||
XCTAssertFalse(room.badges.isDotShown)
|
||||
XCTAssertTrue(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(!room.isHighlighted)
|
||||
#expect(!room.badges.isDotShown)
|
||||
#expect(room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testMentionAndKeywordsUnhighlightedDot() {
|
||||
@Test
|
||||
mutating func mentionAndKeywordsUnhighlightedDot() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 10,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -156,14 +164,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertFalse(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(!room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testMentionAndKeywordsUnhighlightedDotHidden() {
|
||||
@Test
|
||||
mutating func mentionAndKeywordsUnhighlightedDotHidden() {
|
||||
setupRoomSummary(isMarkedUnread: false,
|
||||
unreadMessagesCount: 10,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -173,16 +182,17 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: true)
|
||||
|
||||
XCTAssertFalse(room.isHighlighted)
|
||||
XCTAssertFalse(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(!room.isHighlighted)
|
||||
#expect(!room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
// MARK: - Mark unread
|
||||
|
||||
func testMarkedUnreadDot() {
|
||||
@Test
|
||||
mutating func markedUnreadDot() {
|
||||
setupRoomSummary(isMarkedUnread: true,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -192,14 +202,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testMarkedUnreadDotAndMention() {
|
||||
@Test
|
||||
mutating func markedUnreadDotAndMention() {
|
||||
setupRoomSummary(isMarkedUnread: true,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 5,
|
||||
@@ -209,14 +220,15 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertFalse(room.badges.isCallShown)
|
||||
XCTAssertFalse(room.badges.isMuteShown)
|
||||
XCTAssertTrue(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(!room.badges.isCallShown)
|
||||
#expect(!room.badges.isMuteShown)
|
||||
#expect(room.badges.isMentionShown)
|
||||
}
|
||||
|
||||
func testMarkedUnreadMuteDotAndCall() {
|
||||
@Test
|
||||
mutating func markedUnreadMuteDotAndCall() {
|
||||
setupRoomSummary(isMarkedUnread: true,
|
||||
unreadMessagesCount: 5,
|
||||
unreadMentionsCount: 5,
|
||||
@@ -226,10 +238,10 @@ class HomeScreenRoomTests: XCTestCase {
|
||||
|
||||
let room = HomeScreenRoom(summary: roomSummary, hideUnreadMessagesBadge: false)
|
||||
|
||||
XCTAssertTrue(room.isHighlighted)
|
||||
XCTAssertTrue(room.badges.isDotShown)
|
||||
XCTAssertTrue(room.badges.isCallShown)
|
||||
XCTAssertTrue(room.badges.isMuteShown)
|
||||
XCTAssertFalse(room.badges.isMentionShown)
|
||||
#expect(room.isHighlighted)
|
||||
#expect(room.badges.isDotShown)
|
||||
#expect(room.badges.isCallShown)
|
||||
#expect(room.badges.isMuteShown)
|
||||
#expect(!room.badges.isMentionShown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class HomeScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
final class HomeScreenViewModelTests {
|
||||
var viewModel: HomeScreenViewModelProtocol!
|
||||
var context: HomeScreenViewModelType.Context! {
|
||||
viewModel.context
|
||||
@@ -24,19 +25,18 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUp() {
|
||||
cancellables.removeAll()
|
||||
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testSelectRoom() async {
|
||||
@Test
|
||||
func selectRoom() async {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "mock_room_id"
|
||||
@@ -57,11 +57,12 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
XCTAssertEqual(mockRoomID, selectedRoomID)
|
||||
#expect(correctResult)
|
||||
#expect(mockRoomID == selectedRoomID)
|
||||
}
|
||||
|
||||
func testTapUserAvatar() async {
|
||||
|
||||
@Test
|
||||
func tapUserAvatar() async {
|
||||
setupViewModel()
|
||||
|
||||
var correctResult = false
|
||||
@@ -79,10 +80,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
context.send(viewAction: .showSettings)
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
#expect(correctResult)
|
||||
}
|
||||
|
||||
func testLeaveRoomAlert() async throws {
|
||||
@Test
|
||||
func leaveRoomAlert() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -97,10 +99,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(context.leaveRoomAlertItem?.roomID, mockRoomID)
|
||||
#expect(context.leaveRoomAlertItem?.roomID == mockRoomID)
|
||||
}
|
||||
|
||||
func testLeaveRoomError() async throws {
|
||||
@Test
|
||||
func leaveRoomError() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -108,7 +111,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
room.leaveRoomClosure = { .failure(.sdkError(ClientProxyMockError.generic)) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { value in
|
||||
value.bindings.alertInfo != nil
|
||||
}
|
||||
@@ -116,39 +119,35 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testLeaveRoomSuccess() async {
|
||||
@Test
|
||||
func leaveRoomSuccess() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
var correctResult = false
|
||||
let expectation = expectation(description: #function)
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .roomLeft(let roomIdentifier):
|
||||
correctResult = roomIdentifier == mockRoomID
|
||||
default:
|
||||
break
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let room = JoinedRoomProxyMock(.init(id: mockRoomID, name: "Some room"))
|
||||
room.leaveRoomClosure = { .success(()) }
|
||||
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(room) }
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
if case .roomLeft(let roomIdentifier) = action {
|
||||
return roomIdentifier == mockRoomID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
context.send(viewAction: .confirmLeaveRoom(roomIdentifier: mockRoomID))
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
try await deferred.fulfill()
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testShowRoomDetails() async {
|
||||
@Test
|
||||
func showRoomDetails() async {
|
||||
setupViewModel()
|
||||
|
||||
let mockRoomID = "1"
|
||||
@@ -165,45 +164,49 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
.store(in: &cancellables)
|
||||
context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomID))
|
||||
await Task.yield()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertTrue(correctResult)
|
||||
#expect(context.alertInfo == nil)
|
||||
#expect(correctResult)
|
||||
}
|
||||
|
||||
func testFilters() async throws {
|
||||
@Test
|
||||
func filters() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.filtersState.activateFilter(.people)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 2)
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Foundation and Earth")
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.count == 2)
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Foundation and Earth")
|
||||
}
|
||||
|
||||
func testSearch() async throws {
|
||||
@Test
|
||||
func search() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.isSearchFieldFocused = true
|
||||
context.searchQuery = "lude to Found"
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.first?.name, "Prelude to Foundation")
|
||||
XCTAssertEqual(roomSummaryProvider.roomListPublisher.value.count, 1)
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.first?.name == "Prelude to Foundation")
|
||||
#expect(roomSummaryProvider.roomListPublisher.value.count == 1)
|
||||
}
|
||||
|
||||
func testFiltersEmptyState() async throws {
|
||||
@Test
|
||||
func filtersEmptyState() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.filtersState.activateFilter(.people)
|
||||
context.filtersState.activateFilter(.favourites)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(context.viewState.shouldShowEmptyFilterState)
|
||||
#expect(context.viewState.shouldShowEmptyFilterState)
|
||||
context.isSearchFieldFocused = true
|
||||
XCTAssertFalse(context.viewState.shouldShowEmptyFilterState)
|
||||
#expect(!context.viewState.shouldShowEmptyFilterState)
|
||||
}
|
||||
|
||||
func testSetUpRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func setUpRecoveryBannerState() async throws {
|
||||
// Given a view model without a visible security banner.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
|
||||
// When the recovery state comes through as disabled.
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
|
||||
@@ -211,7 +214,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should be shown to set up recovery.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .show(.setUpRecovery))
|
||||
#expect(context.viewState.securityBannerMode == .show(.setUpRecovery))
|
||||
|
||||
// When the recovery is enabled.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
|
||||
@@ -219,10 +222,11 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should no longer be shown.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
}
|
||||
|
||||
func testDismissSetUpRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func dismissSetUpRecoveryBannerState() async throws {
|
||||
// Given a view model with the setup recovery banner shown.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
@@ -238,16 +242,17 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// And when the recovery state comes through a second time the banner should still not be shown.
|
||||
let failure = deferFailure(context.$viewState, timeout: 1) { $0.securityBannerMode != .dismissed }
|
||||
let failure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.securityBannerMode != .dismissed }
|
||||
securityStateStateSubject.send(.init(verificationState: .verified, recoveryState: .disabled))
|
||||
try await failure.fulfill()
|
||||
}
|
||||
|
||||
func testOutOfSyncRecoveryBannerState() async throws {
|
||||
@Test
|
||||
func outOfSyncRecoveryBannerState() async throws {
|
||||
// Given a view model without a visible security banner.
|
||||
let securityStateStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .unknown))
|
||||
setupViewModel(securityStatePublisher: securityStateStateSubject.asCurrentValuePublisher())
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
|
||||
// When the recovery state comes through as incomplete.
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == true }
|
||||
@@ -255,7 +260,7 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should be shown for out of sync recovery.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .show(.recoveryOutOfSync))
|
||||
#expect(context.viewState.securityBannerMode == .show(.recoveryOutOfSync))
|
||||
|
||||
// When the recovery is enabled.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.requiresExtraAccountSetup == false }
|
||||
@@ -263,16 +268,17 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the banner should no longer be shown.
|
||||
XCTAssertEqual(context.viewState.securityBannerMode, .none)
|
||||
#expect(context.viewState.securityBannerMode == .none)
|
||||
}
|
||||
|
||||
func testInviteUnreadBadge() async throws {
|
||||
@Test
|
||||
func inviteUnreadBadge() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
var invites = context.viewState.rooms.invites
|
||||
XCTAssertEqual(invites.count, 2)
|
||||
#expect(invites.count == 2)
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertTrue(invite.badges.isDotShown)
|
||||
#expect(invite.badges.isDotShown)
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
@@ -285,31 +291,33 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
invites = context.viewState.rooms.invites
|
||||
|
||||
for invite in invites {
|
||||
XCTAssertFalse(invite.badges.isDotShown)
|
||||
#expect(!invite.badges.isDotShown)
|
||||
}
|
||||
}
|
||||
|
||||
func testAcceptInvite() async throws {
|
||||
@Test
|
||||
func acceptInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .presentRoom(roomIdentifier: invitedRoomIDs[0]) }
|
||||
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
}
|
||||
|
||||
func testAcceptSpaceInvite() async throws {
|
||||
@Test
|
||||
func acceptSpaceInvite() async throws {
|
||||
setupViewModel(invites: .spaces)
|
||||
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) {
|
||||
$0 == .presentSpace(SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(id: invitedRoomIDs[0], isSpace: true))))
|
||||
@@ -317,43 +325,48 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .acceptInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertFalse(notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(!notificationManager.removeDeliveredMessageNotificationsForCalled, "The notification will be dismissed when opening the room.")
|
||||
}
|
||||
|
||||
func testDeclineInvite() async throws {
|
||||
@Test
|
||||
func declineInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
try await deferred.fulfill()
|
||||
|
||||
let rejectExpectation = expectation(description: "Expected rejectInvitation to be called.")
|
||||
var rejectCalled = false
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
let roomProxy = InvitedRoomProxyMock(.init())
|
||||
roomProxy.rejectInvitationClosure = {
|
||||
rejectExpectation.fulfill()
|
||||
rejectCalled = true
|
||||
return .success(())
|
||||
}
|
||||
|
||||
return .invited(roomProxy)
|
||||
}
|
||||
context.viewState.bindings.alertInfo?.verticalButtons?[0].action?()
|
||||
await fulfillment(of: [rejectExpectation], timeout: 1.0)
|
||||
|
||||
XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]])
|
||||
XCTAssertTrue(notificationManager.removeDeliveredMessageNotificationsForCalled)
|
||||
XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, [invitedRoomIDs[0]])
|
||||
// Wait for the async action to complete
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
#expect(rejectCalled)
|
||||
|
||||
#expect(appSettings.seenInvites == [invitedRoomIDs[1]])
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForCalled)
|
||||
#expect(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations == [invitedRoomIDs[0]])
|
||||
}
|
||||
|
||||
func testDeclineAndBlockInvite() async throws {
|
||||
@Test
|
||||
func declineAndBlockInvite() async throws {
|
||||
setupViewModel(invites: .rooms)
|
||||
let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID)
|
||||
appSettings.seenInvites = Set(invitedRoomIDs)
|
||||
XCTAssertEqual(invitedRoomIDs.count, 2)
|
||||
#expect(invitedRoomIDs.count == 2)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0]))
|
||||
@@ -364,17 +377,18 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testNewSoundBanner() {
|
||||
@Test
|
||||
func newSoundBanner() {
|
||||
appSettings.hasSeenNewSoundBanner = false
|
||||
|
||||
setupViewModel()
|
||||
XCTAssertTrue(context.viewState.shouldShowBanner)
|
||||
XCTAssertTrue(context.viewState.shouldShowNewSoundBanner)
|
||||
#expect(context.viewState.shouldShowBanner)
|
||||
#expect(context.viewState.shouldShowNewSoundBanner)
|
||||
|
||||
context.send(viewAction: .dismissNewSoundBanner)
|
||||
XCTAssertFalse(context.viewState.shouldShowBanner)
|
||||
XCTAssertFalse(context.viewState.shouldShowNewSoundBanner)
|
||||
XCTAssertTrue(appSettings.hasSeenNewSoundBanner)
|
||||
#expect(!context.viewState.shouldShowBanner)
|
||||
#expect(!context.viewState.shouldShowNewSoundBanner)
|
||||
#expect(appSettings.hasSeenNewSoundBanner)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -382,6 +396,8 @@ class HomeScreenViewModelTests: XCTestCase {
|
||||
enum InviteType { case rooms, spaces }
|
||||
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil) {
|
||||
cancellables.removeAll()
|
||||
|
||||
var rooms: [RoomSummary] = .mockRooms
|
||||
|
||||
switch invites {
|
||||
|
||||
@@ -8,55 +8,60 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class InviteUsersScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct InviteUsersScreenViewModelTests {
|
||||
var viewModel: InviteUsersScreenViewModelProtocol!
|
||||
var userDiscoveryService: UserDiscoveryServiceMock!
|
||||
|
||||
|
||||
var context: InviteUsersScreenViewModel.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testSelectUser() {
|
||||
@Test
|
||||
mutating func selectUser() {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||
roomProxy.inviteUserIDReturnValue = .success(())
|
||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||
|
||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||
#expect(context.viewState.selectedUsers.isEmpty)
|
||||
context.send(viewAction: .toggleUser(.mockAlice))
|
||||
XCTAssertTrue(context.viewState.selectedUsers.count == 1)
|
||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
|
||||
#expect(context.viewState.selectedUsers.count == 1)
|
||||
#expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
|
||||
}
|
||||
|
||||
func testReselectUser() {
|
||||
@Test
|
||||
mutating func reselectUser() {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||
roomProxy.inviteUserIDReturnValue = .success(())
|
||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||
|
||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||
#expect(context.viewState.selectedUsers.isEmpty)
|
||||
context.send(viewAction: .toggleUser(.mockAlice))
|
||||
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
|
||||
#expect(context.viewState.selectedUsers.count == 1)
|
||||
#expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
|
||||
context.send(viewAction: .toggleUser(.mockAlice))
|
||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||
#expect(context.viewState.selectedUsers.isEmpty)
|
||||
}
|
||||
|
||||
func testDeselectUser() {
|
||||
@Test
|
||||
mutating func deselectUser() {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "newroom", members: []))
|
||||
roomProxy.inviteUserIDReturnValue = .success(())
|
||||
setupViewModel(roomProxy: roomProxy, isSkippable: true)
|
||||
|
||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||
#expect(context.viewState.selectedUsers.isEmpty)
|
||||
context.send(viewAction: .toggleUser(.mockAlice))
|
||||
XCTAssertEqual(context.viewState.selectedUsers.count, 1)
|
||||
XCTAssertEqual(context.viewState.selectedUsers.first?.userID, UserProfileProxy.mockAlice.userID)
|
||||
#expect(context.viewState.selectedUsers.count == 1)
|
||||
#expect(context.viewState.selectedUsers.first?.userID == UserProfileProxy.mockAlice.userID)
|
||||
context.send(viewAction: .toggleUser(.mockAlice))
|
||||
XCTAssertTrue(context.viewState.selectedUsers.isEmpty)
|
||||
#expect(context.viewState.selectedUsers.isEmpty)
|
||||
}
|
||||
|
||||
func testInviteButton() async throws {
|
||||
|
||||
@Test
|
||||
mutating func inviteButton() async throws {
|
||||
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "test", members: mockedMembers))
|
||||
roomProxy.inviteUserIDReturnValue = .success(())
|
||||
@@ -80,10 +85,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .proceed)
|
||||
|
||||
try await deferredAction.fulfill()
|
||||
XCTAssertEqual(roomProxy.inviteUserIDReceivedInvocations, [RoomMemberProxyMock.mockAlice.userID])
|
||||
#expect(roomProxy.inviteUserIDReceivedInvocations == [RoomMemberProxyMock.mockAlice.userID])
|
||||
}
|
||||
|
||||
private func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) {
|
||||
private mutating func setupViewModel(roomProxy: JoinedRoomProxyProtocol, isSkippable: Bool) {
|
||||
userDiscoveryService = UserDiscoveryServiceMock()
|
||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||
let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
|
||||
@@ -38,7 +38,7 @@ class JoinRoomScreenViewModelTests: XCTestCase {
|
||||
clientProxy = nil
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
|
||||
func testInteraction() async throws {
|
||||
XCTAssertTrue(appSettings.seenInvites.isEmpty, "There shouldn't be any seen invites before running the tests.")
|
||||
|
||||
|
||||
@@ -7,22 +7,25 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import KeychainAccess
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class KeychainControllerTests: XCTestCase {
|
||||
var keychain: KeychainController!
|
||||
@Suite
|
||||
struct KeychainControllerTests {
|
||||
var keychain: KeychainController
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
keychain = KeychainController(service: .tests,
|
||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
keychain.removeAllRestorationTokens()
|
||||
keychain.resetSecrets()
|
||||
}
|
||||
|
||||
func testAddRestorationToken() {
|
||||
@Test
|
||||
func addRestorationToken() {
|
||||
// Given an empty keychain.
|
||||
XCTAssertTrue(keychain.restorationTokens().isEmpty, "The keychain should be empty to begin with.")
|
||||
#expect(keychain.restorationTokens().isEmpty, "The keychain should be empty to begin with.")
|
||||
|
||||
// When adding an restoration token.
|
||||
let username = "@test:example.com"
|
||||
@@ -39,10 +42,11 @@ class KeychainControllerTests: XCTestCase {
|
||||
keychain.setRestorationToken(restorationToken, forUsername: username)
|
||||
|
||||
// Then the restoration token should be stored in the keychain.
|
||||
XCTAssertEqual(keychain.restorationTokenForUsername(username), restorationToken, "The retrieved restoration token should match the value that was stored.")
|
||||
#expect(keychain.restorationTokenForUsername(username) == restorationToken, "The retrieved restoration token should match the value that was stored.")
|
||||
}
|
||||
|
||||
func testRemovingRestorationToken() {
|
||||
@Test
|
||||
func removingRestorationToken() {
|
||||
// Given a keychain with a stored restoration token.
|
||||
let username = "@test:example.com"
|
||||
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
|
||||
@@ -56,18 +60,19 @@ class KeychainControllerTests: XCTestCase {
|
||||
passphrase: "passphrase",
|
||||
pusherNotificationClientIdentifier: "pusherClientID")
|
||||
keychain.setRestorationToken(restorationToken, forUsername: username)
|
||||
XCTAssertEqual(keychain.restorationTokens().count, 1, "The keychain should have 1 restoration token.")
|
||||
XCTAssertEqual(keychain.restorationTokenForUsername(username), restorationToken, "The initial restoration token should match the value that was stored.")
|
||||
#expect(keychain.restorationTokens().count == 1, "The keychain should have 1 restoration token.")
|
||||
#expect(keychain.restorationTokenForUsername(username) == restorationToken, "The initial restoration token should match the value that was stored.")
|
||||
|
||||
// When deleting the restoration token.
|
||||
keychain.removeRestorationTokenForUsername(username)
|
||||
|
||||
// Then the keychain should be empty.
|
||||
XCTAssertTrue(keychain.restorationTokens().isEmpty, "The keychain should be empty after deleting the token.")
|
||||
XCTAssertNil(keychain.restorationTokenForUsername(username), "There restoration token should not be returned after removal.")
|
||||
#expect(keychain.restorationTokens().isEmpty, "The keychain should be empty after deleting the token.")
|
||||
#expect(keychain.restorationTokenForUsername(username) == nil, "There restoration token should not be returned after removal.")
|
||||
}
|
||||
|
||||
func testRemovingAllRestorationTokens() {
|
||||
@Test
|
||||
func removingAllRestorationTokens() {
|
||||
// Given a keychain with 5 stored restoration tokens.
|
||||
for index in 0..<5 {
|
||||
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
|
||||
@@ -82,16 +87,17 @@ class KeychainControllerTests: XCTestCase {
|
||||
pusherNotificationClientIdentifier: "pusherClientID")
|
||||
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com")
|
||||
}
|
||||
XCTAssertEqual(keychain.restorationTokens().count, 5, "The keychain should have 5 restoration tokens.")
|
||||
#expect(keychain.restorationTokens().count == 5, "The keychain should have 5 restoration tokens.")
|
||||
|
||||
// When deleting all of the restoration tokens.
|
||||
keychain.removeAllRestorationTokens()
|
||||
|
||||
// Then the keychain should be empty.
|
||||
XCTAssertTrue(keychain.restorationTokens().isEmpty, "The keychain should be empty after deleting the token.")
|
||||
#expect(keychain.restorationTokens().isEmpty, "The keychain should be empty after deleting the token.")
|
||||
}
|
||||
|
||||
func testRemovingSingleRestorationTokens() {
|
||||
@Test
|
||||
func removingSingleRestorationTokens() {
|
||||
// Given a keychain with 5 stored restoration tokens.
|
||||
for index in 0..<5 {
|
||||
let restorationToken = RestorationToken(session: .init(accessToken: "accessToken",
|
||||
@@ -106,137 +112,140 @@ class KeychainControllerTests: XCTestCase {
|
||||
pusherNotificationClientIdentifier: "pusherClientID")
|
||||
keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com")
|
||||
}
|
||||
XCTAssertEqual(keychain.restorationTokens().count, 5, "The keychain should have 5 restoration tokens.")
|
||||
#expect(keychain.restorationTokens().count == 5, "The keychain should have 5 restoration tokens.")
|
||||
|
||||
// When deleting one of the restoration tokens.
|
||||
keychain.removeRestorationTokenForUsername("@test2:example.com")
|
||||
|
||||
// Then the other 4 items should remain untouched.
|
||||
XCTAssertEqual(keychain.restorationTokens().count, 4, "The keychain have 4 remaining restoration tokens.")
|
||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test0:example.com"), "The restoration token should not have been deleted.")
|
||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test1:example.com"), "The restoration token should not have been deleted.")
|
||||
XCTAssertNil(keychain.restorationTokenForUsername("@test2:example.com"), "The restoration token should have been deleted.")
|
||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test3:example.com"), "The restoration token should not have been deleted.")
|
||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test4:example.com"), "The restoration token should not have been deleted.")
|
||||
#expect(keychain.restorationTokens().count == 4, "The keychain have 4 remaining restoration tokens.")
|
||||
#expect(keychain.restorationTokenForUsername("@test0:example.com") != nil, "The restoration token should not have been deleted.")
|
||||
#expect(keychain.restorationTokenForUsername("@test1:example.com") != nil, "The restoration token should not have been deleted.")
|
||||
#expect(keychain.restorationTokenForUsername("@test2:example.com") == nil, "The restoration token should have been deleted.")
|
||||
#expect(keychain.restorationTokenForUsername("@test3:example.com") != nil, "The restoration token should not have been deleted.")
|
||||
#expect(keychain.restorationTokenForUsername("@test4:example.com") != nil, "The restoration token should not have been deleted.")
|
||||
}
|
||||
|
||||
func testUnsupportedRestorationToken() {
|
||||
@Test
|
||||
func unsupportedRestorationToken() throws {
|
||||
// Given a keychain with an unsupported restoration token with a sliding sync proxy URL value.
|
||||
let underlyingKeychain = Keychain(service: KeychainControllerService.tests.restorationTokenID,
|
||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
// Note: We assert with this underlying keychain's keys as keychain.restorationTokens() triggers the deletion that we're testing.
|
||||
XCTAssertTrue(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty to begin with.")
|
||||
#expect(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty to begin with.")
|
||||
|
||||
do {
|
||||
let unsupportedToken = RestorationTokenV4(session: SessionV1(accessToken: "1234",
|
||||
refreshToken: nil,
|
||||
userId: "@test:example.com",
|
||||
deviceId: "D3V1C3",
|
||||
homeserverUrl: "https://matrix.example.com",
|
||||
oidcData: nil,
|
||||
slidingSyncVersion: .proxy(url: "https://sync.example.com")),
|
||||
sessionDirectory: .sessionsBaseDirectory.appending(component: UUID().uuidString),
|
||||
passphrase: "passphrase",
|
||||
pusherNotificationClientIdentifier: "pusherClientID")
|
||||
let tokenData = try JSONEncoder().encode(unsupportedToken)
|
||||
try underlyingKeychain.set(tokenData, key: "@test:example.com")
|
||||
XCTAssertEqual(underlyingKeychain.allKeys().count, 1)
|
||||
} catch {
|
||||
XCTFail("Failed storing user restore token with error: \(error)")
|
||||
}
|
||||
let unsupportedToken = RestorationTokenV4(session: SessionV1(accessToken: "1234",
|
||||
refreshToken: nil,
|
||||
userId: "@test:example.com",
|
||||
deviceId: "D3V1C3",
|
||||
homeserverUrl: "https://matrix.example.com",
|
||||
oidcData: nil,
|
||||
slidingSyncVersion: .proxy(url: "https://sync.example.com")),
|
||||
sessionDirectory: .sessionsBaseDirectory.appending(component: UUID().uuidString),
|
||||
passphrase: "passphrase",
|
||||
pusherNotificationClientIdentifier: "pusherClientID")
|
||||
let tokenData = try JSONEncoder().encode(unsupportedToken)
|
||||
try underlyingKeychain.set(tokenData, key: "@test:example.com")
|
||||
#expect(underlyingKeychain.allKeys().count == 1)
|
||||
|
||||
// When attempting to retrieve the unsupported token.
|
||||
let retrievedToken = keychain.restorationTokenForUsername("@test:example.com")
|
||||
|
||||
// Then nothing should be returned and the restoration token should be automatically removed.
|
||||
XCTAssertNil(retrievedToken, "The token should not be decoded.")
|
||||
XCTAssertTrue(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty again.")
|
||||
#expect(retrievedToken == nil, "The token should not be decoded.")
|
||||
#expect(underlyingKeychain.allKeys().isEmpty, "The keychain should be empty again.")
|
||||
}
|
||||
|
||||
func testAddPINCode() throws {
|
||||
@Test
|
||||
func addPINCode() throws {
|
||||
// Given a keychain without a PIN code set.
|
||||
try XCTAssertFalse(keychain.containsPINCode(), "A new keychain shouldn't contain a PIN code.")
|
||||
XCTAssertNil(keychain.pinCode(), "A new keychain shouldn't return a PIN code.")
|
||||
#expect(try !keychain.containsPINCode(), "A new keychain shouldn't contain a PIN code.")
|
||||
#expect(keychain.pinCode() == nil, "A new keychain shouldn't return a PIN code.")
|
||||
|
||||
// When setting a PIN code.
|
||||
try keychain.setPINCode("0000")
|
||||
|
||||
// Then the PIN code should be stored.
|
||||
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||
#expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
#expect(keychain.pinCode() == "0000", "The stored PIN code should match what was set.")
|
||||
}
|
||||
|
||||
func testUpdatePINCode() throws {
|
||||
@Test
|
||||
func updatePINCode() throws {
|
||||
// Given a keychain with a PIN code already set.
|
||||
try keychain.setPINCode("0000")
|
||||
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||
#expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
#expect(keychain.pinCode() == "0000", "The stored PIN code should match what was set.")
|
||||
|
||||
// When setting a different PIN code.
|
||||
try keychain.setPINCode("1234")
|
||||
|
||||
// Then the PIN code should be updated.
|
||||
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should still contain the PIN code.")
|
||||
XCTAssertEqual(keychain.pinCode(), "1234", "The stored PIN code should match the new value.")
|
||||
#expect(try keychain.containsPINCode(), "The keychain should still contain the PIN code.")
|
||||
#expect(keychain.pinCode() == "1234", "The stored PIN code should match the new value.")
|
||||
}
|
||||
|
||||
func testRemovePINCode() throws {
|
||||
@Test
|
||||
func removePINCode() throws {
|
||||
// Given a keychain with a PIN code already set.
|
||||
try keychain.setPINCode("0000")
|
||||
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||
#expect(try keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||
#expect(keychain.pinCode() == "0000", "The stored PIN code should match what was set.")
|
||||
|
||||
// When removing the PIN code.
|
||||
keychain.removePINCode()
|
||||
|
||||
// Then the PIN code should no longer be stored.
|
||||
try XCTAssertFalse(keychain.containsPINCode(), "The keychain should no longer contain the PIN code.")
|
||||
XCTAssertNil(keychain.pinCode(), "There shouldn't be a stored PIN code after removing it.")
|
||||
#expect(try !keychain.containsPINCode(), "The keychain should no longer contain the PIN code.")
|
||||
#expect(keychain.pinCode() == nil, "There shouldn't be a stored PIN code after removing it.")
|
||||
}
|
||||
|
||||
func testAddPINCodeBiometricState() throws {
|
||||
@Test
|
||||
func addPINCodeBiometricState() throws {
|
||||
// Given a keychain without any biometric state.
|
||||
XCTAssertFalse(keychain.containsPINCodeBiometricState(), "A new keychain shouldn't contain biometric state.")
|
||||
XCTAssertNil(keychain.pinCodeBiometricState(), "A new keychain shouldn't return biometric state.")
|
||||
#expect(!keychain.containsPINCodeBiometricState(), "A new keychain shouldn't contain biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() == nil, "A new keychain shouldn't return biometric state.")
|
||||
|
||||
// When setting the state.
|
||||
let data = Data("Face ID".utf8)
|
||||
try keychain.setPINCodeBiometricState(data)
|
||||
|
||||
// Then the state should be stored.
|
||||
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.")
|
||||
#expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() == data, "The stored biometric state should match what was set.")
|
||||
}
|
||||
|
||||
func testUpdatePINCodeBiometricState() throws {
|
||||
@Test
|
||||
func updatePINCodeBiometricState() throws {
|
||||
// Given a keychain that contains PIN code biometric state.
|
||||
let data = Data("😃".utf8)
|
||||
try keychain.setPINCodeBiometricState(data)
|
||||
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.")
|
||||
#expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() == data, "The stored biometric state should match what was set.")
|
||||
|
||||
// When setting different state.
|
||||
let newData = Data("😎".utf8)
|
||||
try keychain.setPINCodeBiometricState(newData)
|
||||
|
||||
// Then the state should be updated.
|
||||
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should still contain biometric state.")
|
||||
XCTAssertNotEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state shouldn't match the old value.")
|
||||
XCTAssertEqual(keychain.pinCodeBiometricState(), newData, "The stored biometric state should match the new value.")
|
||||
#expect(keychain.containsPINCodeBiometricState(), "The keychain should still contain biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() != data, "The stored biometric state shouldn't match the old value.")
|
||||
#expect(keychain.pinCodeBiometricState() == newData, "The stored biometric state should match the new value.")
|
||||
}
|
||||
|
||||
func testRemovePINCodeBiometricState() throws {
|
||||
@Test
|
||||
func removePINCodeBiometricState() throws {
|
||||
// Given a keychain that contains PIN code biometric state.
|
||||
let data = Data("Face ID".utf8)
|
||||
try keychain.setPINCodeBiometricState(data)
|
||||
XCTAssertTrue(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
XCTAssertEqual(keychain.pinCodeBiometricState(), data, "The stored biometric state should match what was set.")
|
||||
#expect(keychain.containsPINCodeBiometricState(), "The keychain should contain the biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() == data, "The stored biometric state should match what was set.")
|
||||
|
||||
// When removing the state.
|
||||
keychain.removePINCodeBiometricState()
|
||||
|
||||
// Then the state should no longer be stored.
|
||||
XCTAssertFalse(keychain.containsPINCodeBiometricState(), "The keychain should no longer contain the biometric state.")
|
||||
XCTAssertNil(keychain.pinCodeBiometricState(), "There shouldn't be any stored biometric state after removing it.")
|
||||
#expect(!keychain.containsPINCodeBiometricState(), "The keychain should no longer contain the biometric state.")
|
||||
#expect(keychain.pinCodeBiometricState() == nil, "There shouldn't be any stored biometric state after removing it.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,25 +7,22 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
var viewModel: KnockRequestsListScreenViewModelProtocol!
|
||||
|
||||
var context: KnockRequestsListScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
@Suite
|
||||
struct KnockRequestsListScreenViewModelTests {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testLoadingState() async throws {
|
||||
@Test
|
||||
func loadingState() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
!state.shouldDisplayRequests &&
|
||||
@@ -39,11 +36,13 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testEmptyState() async throws {
|
||||
@Test
|
||||
func emptyState() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
!state.shouldDisplayRequests &&
|
||||
@@ -57,7 +56,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testLoadedState() async throws {
|
||||
@Test
|
||||
func loadedState() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
|
||||
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||
@@ -65,9 +65,10 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
|
||||
joinRule: .knock))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.shouldDisplayRequests &&
|
||||
@@ -99,10 +100,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .declineRequest(eventID: "2"))
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let declineAlertInfo = context.alertInfo else {
|
||||
XCTFail("Can't be nil")
|
||||
return
|
||||
}
|
||||
let declineAlertInfo = try #require(context.alertInfo)
|
||||
deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.shouldDisplayRequests &&
|
||||
state.handledEventIDs == ["1", "2"] &&
|
||||
@@ -119,10 +117,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .ban(eventID: "3"))
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let banAlertInfo = context.alertInfo else {
|
||||
XCTFail("Can't be nil")
|
||||
return
|
||||
}
|
||||
let banAlertInfo = try #require(context.alertInfo)
|
||||
deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.shouldDisplayRequests &&
|
||||
state.handledEventIDs == ["1", "2", "3"] &&
|
||||
@@ -134,15 +129,17 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAcceptAll() async throws {
|
||||
@Test
|
||||
func acceptAll() async throws {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
|
||||
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||
joinRule: .knock))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.shouldDisplayRequests &&
|
||||
@@ -164,10 +161,7 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .acceptAllRequests)
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let alertInfo = context.alertInfo else {
|
||||
XCTFail("Can't be nil")
|
||||
return
|
||||
}
|
||||
let alertInfo = try #require(context.alertInfo)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { state in
|
||||
!state.shouldDisplayRequests &&
|
||||
@@ -179,7 +173,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testLoadedStateBecomesEmptyIfTheJoinRuleIsNotKnocking() async throws {
|
||||
@Test
|
||||
func loadedStateBecomesEmptyIfTheJoinRuleIsNotKnocking() async throws {
|
||||
// If there is a sudden change in the rule, but the requests are still published, we want to hide all of them and show the empty view
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
|
||||
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||
@@ -188,9 +183,10 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
|
||||
joinRule: .invite))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
!state.shouldDisplayRequests &&
|
||||
@@ -201,7 +197,8 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testLoadedStateBecomesEmptyIfPermissionsAreRemoved() async throws {
|
||||
@Test
|
||||
func loadedStateBecomesEmptyIfPermissionsAreRemoved() async throws {
|
||||
// If there is a sudden change in permissions, and the user can't do any other action, we hide all the requests and shoe the empty view
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||
@@ -209,9 +206,10 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
||||
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||
joinRule: .knock,
|
||||
powerLevelsConfiguration: .init(canUserInvite: false)))
|
||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||
mediaProvider: MediaProviderMock(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
let context = viewModel.context
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
!state.shouldDisplayRequests &&
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class LegalInformationScreenViewModelTests: XCTestCase { }
|
||||
@@ -7,72 +7,79 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class LocalizationTests: XCTestCase {
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
@Suite
|
||||
final class LocalizationTests {
|
||||
deinit {
|
||||
Bundle.overrideLocalizations = nil
|
||||
}
|
||||
|
||||
|
||||
/// Test ElementL10n considers app language changes
|
||||
func testAppLanguage() {
|
||||
@Test
|
||||
func appLanguage() {
|
||||
// set app language to English
|
||||
Bundle.overrideLocalizations = ["en"]
|
||||
|
||||
XCTAssertEqual(L10n.testLanguageIdentifier, "en")
|
||||
|
||||
|
||||
#expect(L10n.testLanguageIdentifier == "en")
|
||||
|
||||
// set app language to Italian
|
||||
Bundle.overrideLocalizations = ["it"]
|
||||
|
||||
XCTAssertEqual(L10n.testLanguageIdentifier, "it")
|
||||
|
||||
#expect(L10n.testLanguageIdentifier == "it")
|
||||
}
|
||||
|
||||
|
||||
/// Test fallback language for a language not supported at all
|
||||
func testFallbackOnNotSupportedLanguage() {
|
||||
@Test
|
||||
func fallbackOnNotSupportedLanguage() {
|
||||
// set app language to something Element don't support at all (chose non existing identifier)
|
||||
Bundle.overrideLocalizations = ["xx"]
|
||||
|
||||
XCTAssertEqual(L10n.testLanguageIdentifier, "en")
|
||||
|
||||
#expect(L10n.testLanguageIdentifier == "en")
|
||||
}
|
||||
|
||||
|
||||
/// Test fallback language for a language supported but poorly translated
|
||||
func testFallbackOnNotTranslatedKey() {
|
||||
@Test
|
||||
func fallbackOnNotTranslatedKey() {
|
||||
// set app language to something Element supports but use a key that is not translated (we have a key that should never be translated)
|
||||
Bundle.overrideLocalizations = ["it"]
|
||||
|
||||
XCTAssertEqual(L10n.testLanguageIdentifier, "it")
|
||||
XCTAssertEqual(L10n.testUntranslatedDefaultLanguageIdentifier, "en")
|
||||
|
||||
#expect(L10n.testLanguageIdentifier == "it")
|
||||
#expect(L10n.testUntranslatedDefaultLanguageIdentifier == "en")
|
||||
}
|
||||
|
||||
|
||||
/// Test plurals that ElementL10n considers app language changes
|
||||
func testPlurals() {
|
||||
@Test
|
||||
func plurals() {
|
||||
// set app language to English
|
||||
Bundle.overrideLocalizations = ["en"]
|
||||
|
||||
XCTAssertEqual(L10n.commonMemberCount(1), "1 Member")
|
||||
XCTAssertEqual(L10n.commonMemberCount(2), "2 Members")
|
||||
|
||||
|
||||
#expect(L10n.commonMemberCount(1) == "1 Member")
|
||||
#expect(L10n.commonMemberCount(2) == "2 Members")
|
||||
|
||||
// set app language to Italian
|
||||
Bundle.overrideLocalizations = ["it"]
|
||||
|
||||
XCTAssertEqual(L10n.commonMemberCount(1), "1 Membro")
|
||||
XCTAssertEqual(L10n.commonMemberCount(2), "2 Membri")
|
||||
|
||||
#expect(L10n.commonMemberCount(1) == "1 Membro")
|
||||
#expect(L10n.commonMemberCount(2) == "2 Membri")
|
||||
}
|
||||
|
||||
|
||||
/// Test plurals fallback language for a language not supported at all
|
||||
func testPluralsFallbackOnNotSupportedLanguage() {
|
||||
@Test
|
||||
func pluralsFallbackOnNotSupportedLanguage() {
|
||||
// set app language to something Element don't support at all ("invalid identifier")
|
||||
Bundle.overrideLocalizations = ["xx"]
|
||||
|
||||
XCTAssertEqual(L10n.commonMemberCount(1), "1 Member")
|
||||
XCTAssertEqual(L10n.commonMemberCount(2), "2 Members")
|
||||
|
||||
#expect(L10n.commonMemberCount(1) == "1 Member")
|
||||
#expect(L10n.commonMemberCount(2) == "2 Members")
|
||||
}
|
||||
|
||||
|
||||
/// Test untranslated strings
|
||||
func testUntranslated() {
|
||||
XCTAssertEqual(UntranslatedL10n.untranslated, "Untranslated")
|
||||
XCTAssertEqual(UntranslatedL10n.untranslatedPlural(1), "One untranslated item")
|
||||
XCTAssertEqual(UntranslatedL10n.untranslatedPlural(5), "5 untranslated items")
|
||||
@Test
|
||||
func untranslated() {
|
||||
#expect(UntranslatedL10n.untranslated == "Untranslated")
|
||||
#expect(UntranslatedL10n.untranslatedPlural(1) == "One untranslated item")
|
||||
#expect(UntranslatedL10n.untranslatedPlural(5) == "5 untranslated items")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,65 +7,66 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
@testable import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class LoggingTests: XCTestCase {
|
||||
@Suite
|
||||
final class LoggingTests {
|
||||
private enum Constants {
|
||||
static let genericFailure = "Test failed"
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
deinit {
|
||||
Tracing.logsDirectoryOverride = nil
|
||||
try reloadTracingFileWriter(configuration: .init(path: URL.appGroupLogsDirectory.path(percentEncoded: false),
|
||||
filePrefix: "console-tests",
|
||||
fileSuffix: ".log",
|
||||
maxTotalSizeBytes: 1000,
|
||||
maxAgeSeconds: 1000))
|
||||
do {
|
||||
try reloadTracingFileWriter(configuration: .init(path: URL.appGroupLogsDirectory.path(percentEncoded: false),
|
||||
filePrefix: "console-tests",
|
||||
fileSuffix: ".log",
|
||||
maxTotalSizeBytes: 1000,
|
||||
maxAgeSeconds: 1000))
|
||||
} catch {
|
||||
Issue.record(error)
|
||||
}
|
||||
}
|
||||
|
||||
func testFileLogging() throws {
|
||||
@Test
|
||||
func fileLogging() throws {
|
||||
try setupTest()
|
||||
|
||||
let infoLog = UUID().uuidString
|
||||
MXLog.info(infoLog)
|
||||
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
try XCTAssertTrue(String(contentsOf: logFile, encoding: .utf8).contains(infoLog))
|
||||
#expect(try String(contentsOf: logFile, encoding: .utf8).contains(infoLog))
|
||||
}
|
||||
|
||||
func testLogLevels() throws {
|
||||
|
||||
@Test
|
||||
func logLevels() throws {
|
||||
try setupTest()
|
||||
|
||||
let verboseLog = UUID().uuidString
|
||||
MXLog.verbose(verboseLog)
|
||||
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
try XCTAssertFalse(String(contentsOf: logFile, encoding: .utf8).contains(verboseLog))
|
||||
#expect(try !String(contentsOf: logFile, encoding: .utf8).contains(verboseLog))
|
||||
}
|
||||
|
||||
/// This is meant to test the `Target.tests.configure(…)`, but at this stage the test is somewhat pointless
|
||||
/// as it is unlikely to have been called before `tearDown` has manually set the file prefix 😕.
|
||||
func testTargetName() {
|
||||
@Test
|
||||
func targetName() throws {
|
||||
MXLog.info(UUID().uuidString)
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
let target = "tests"
|
||||
XCTAssertTrue(logFile.lastPathComponent.contains(target))
|
||||
#expect(logFile.lastPathComponent.contains(target))
|
||||
}
|
||||
|
||||
func testRoomSummaryContentIsRedacted() throws {
|
||||
@Test
|
||||
func roomSummaryContentIsRedacted() throws {
|
||||
try setupTest()
|
||||
|
||||
// Given a room summary that contains sensitive information
|
||||
@@ -99,19 +100,17 @@ class LoggingTests: XCTestCase {
|
||||
MXLog.info(roomSummary)
|
||||
|
||||
// Then the log file should not include the sensitive information
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
let content = try String(contentsOf: logFile, encoding: .utf8)
|
||||
XCTAssertTrue(content.contains(roomSummary.id))
|
||||
XCTAssertFalse(content.contains(roomName))
|
||||
XCTAssertFalse(content.contains(lastMessage))
|
||||
XCTAssertFalse(content.contains(heroName))
|
||||
#expect(content.contains(roomSummary.id))
|
||||
#expect(!content.contains(roomName))
|
||||
#expect(!content.contains(lastMessage))
|
||||
#expect(!content.contains(heroName))
|
||||
}
|
||||
|
||||
func testTimelineContentIsRedacted() throws {
|
||||
|
||||
@Test
|
||||
func timelineContentIsRedacted() throws {
|
||||
try setupTest()
|
||||
|
||||
// Given timeline items that contain text
|
||||
@@ -181,35 +180,33 @@ class LoggingTests: XCTestCase {
|
||||
MXLog.info(fileMessage)
|
||||
|
||||
// Then the log file should not include the text content
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
let content = try String(contentsOf: logFile, encoding: .utf8)
|
||||
XCTAssertTrue(content.contains(textMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(textMessage.body))
|
||||
XCTAssertFalse(content.contains(textAttributedString))
|
||||
#expect(content.contains(textMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(textMessage.body))
|
||||
#expect(!content.contains(textAttributedString))
|
||||
|
||||
XCTAssertTrue(content.contains(noticeMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(noticeMessage.body))
|
||||
XCTAssertFalse(content.contains(noticeAttributedString))
|
||||
#expect(content.contains(noticeMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(noticeMessage.body))
|
||||
#expect(!content.contains(noticeAttributedString))
|
||||
|
||||
XCTAssertTrue(content.contains(emoteMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(emoteMessage.body))
|
||||
XCTAssertFalse(content.contains(emoteAttributedString))
|
||||
#expect(content.contains(emoteMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(emoteMessage.body))
|
||||
#expect(!content.contains(emoteAttributedString))
|
||||
|
||||
XCTAssertTrue(content.contains(imageMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(imageMessage.body))
|
||||
#expect(content.contains(imageMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(imageMessage.body))
|
||||
|
||||
XCTAssertTrue(content.contains(videoMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(videoMessage.body))
|
||||
#expect(content.contains(videoMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(videoMessage.body))
|
||||
|
||||
XCTAssertTrue(content.contains(fileMessage.id.uniqueID.value))
|
||||
XCTAssertFalse(content.contains(fileMessage.body))
|
||||
#expect(content.contains(fileMessage.id.uniqueID.value))
|
||||
#expect(!content.contains(fileMessage.body))
|
||||
}
|
||||
|
||||
func testRustMessageContentIsRedacted() throws {
|
||||
|
||||
@Test
|
||||
func rustMessageContentIsRedacted() throws {
|
||||
try setupTest()
|
||||
|
||||
// Given message content that contain text
|
||||
@@ -250,36 +247,34 @@ class LoggingTests: XCTestCase {
|
||||
MXLog.info(rustFileMessage)
|
||||
|
||||
// Then the log file should not include the text content
|
||||
guard let logFile = Tracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
|
||||
let logFile = try #require(Tracing.logFiles.first)
|
||||
|
||||
let content = try String(contentsOf: logFile, encoding: .utf8)
|
||||
XCTAssertTrue(content.contains(String(describing: TextMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(textString))
|
||||
#expect(content.contains(String(describing: TextMessageContent.self)))
|
||||
#expect(!content.contains(textString))
|
||||
|
||||
XCTAssertTrue(content.contains(String(describing: NoticeMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(noticeString))
|
||||
#expect(content.contains(String(describing: NoticeMessageContent.self)))
|
||||
#expect(!content.contains(noticeString))
|
||||
|
||||
XCTAssertTrue(content.contains(String(describing: EmoteMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(emoteString))
|
||||
#expect(content.contains(String(describing: EmoteMessageContent.self)))
|
||||
#expect(!content.contains(emoteString))
|
||||
|
||||
XCTAssertTrue(content.contains(String(describing: ImageMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(rustImageMessage.filename))
|
||||
#expect(content.contains(String(describing: ImageMessageContent.self)))
|
||||
#expect(!content.contains(rustImageMessage.filename))
|
||||
|
||||
XCTAssertTrue(content.contains(String(describing: VideoMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(rustVideoMessage.filename))
|
||||
#expect(content.contains(String(describing: VideoMessageContent.self)))
|
||||
#expect(!content.contains(rustVideoMessage.filename))
|
||||
|
||||
XCTAssertTrue(content.contains(String(describing: FileMessageContent.self)))
|
||||
XCTAssertFalse(content.contains(rustFileMessage.filename))
|
||||
#expect(content.contains(String(describing: FileMessageContent.self)))
|
||||
#expect(!content.contains(rustFileMessage.filename))
|
||||
}
|
||||
|
||||
func testLogFileSorting() throws {
|
||||
@Test
|
||||
func logFileSorting() throws {
|
||||
try setupTest(redirectTracingFileWriter: false)
|
||||
|
||||
// Given a collection of log files.
|
||||
XCTAssertTrue(Tracing.logFiles.isEmpty)
|
||||
#expect(Tracing.logFiles.isEmpty)
|
||||
|
||||
// When creating new logs.
|
||||
let logsFileDirectory = Tracing.logsDirectory
|
||||
@@ -294,17 +289,17 @@ class LoggingTests: XCTestCase {
|
||||
}
|
||||
|
||||
// Then the logs should be sorted chronologically (newest first) and not alphabetically.
|
||||
XCTAssertEqual(Tracing.logFiles.map(\.lastPathComponent),
|
||||
["console-nse.5.log",
|
||||
"console-nse.4.log",
|
||||
"console-nse.3.log",
|
||||
"console-nse.2.log",
|
||||
"console-nse.1.log",
|
||||
"console.5.log",
|
||||
"console.4.log",
|
||||
"console.3.log",
|
||||
"console.2.log",
|
||||
"console.1.log"])
|
||||
#expect(Tracing.logFiles.map(\.lastPathComponent) ==
|
||||
["console-nse.5.log",
|
||||
"console-nse.4.log",
|
||||
"console-nse.3.log",
|
||||
"console-nse.2.log",
|
||||
"console-nse.1.log",
|
||||
"console.5.log",
|
||||
"console.4.log",
|
||||
"console.3.log",
|
||||
"console.2.log",
|
||||
"console.1.log"])
|
||||
|
||||
// When updating the oldest log file.
|
||||
let currentLogFile = logsFileDirectory.appending(path: "console.1.log")
|
||||
@@ -314,17 +309,17 @@ class LoggingTests: XCTestCase {
|
||||
try fileHandle.close()
|
||||
|
||||
// Then that file should now be the first log file.
|
||||
XCTAssertEqual(Tracing.logFiles.map(\.lastPathComponent),
|
||||
["console.1.log",
|
||||
"console-nse.5.log",
|
||||
"console-nse.4.log",
|
||||
"console-nse.3.log",
|
||||
"console-nse.2.log",
|
||||
"console-nse.1.log",
|
||||
"console.5.log",
|
||||
"console.4.log",
|
||||
"console.3.log",
|
||||
"console.2.log"])
|
||||
#expect(Tracing.logFiles.map(\.lastPathComponent) ==
|
||||
["console.1.log",
|
||||
"console-nse.5.log",
|
||||
"console-nse.4.log",
|
||||
"console-nse.3.log",
|
||||
"console-nse.2.log",
|
||||
"console-nse.1.log",
|
||||
"console.5.log",
|
||||
"console.4.log",
|
||||
"console.3.log",
|
||||
"console.2.log"])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -339,7 +334,7 @@ class LoggingTests: XCTestCase {
|
||||
|
||||
// Make an assertion before redirecting the logs as it the SDK is likely to put an empty file
|
||||
// in the directory, ready to be written to.
|
||||
XCTAssertTrue(Tracing.logFiles.isEmpty)
|
||||
#expect(Tracing.logFiles.isEmpty)
|
||||
|
||||
if redirectTracingFileWriter {
|
||||
try reloadTracingFileWriter(configuration: .init(path: testDirectory.path(percentEncoded: false),
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class LoginScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct LoginScreenViewModelTests {
|
||||
var viewModel: LoginScreenViewModelProtocol!
|
||||
var context: LoginScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
@@ -19,161 +20,214 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
var clientFactory: AuthenticationClientFactoryMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
|
||||
func testBasicServer() async {
|
||||
@Test
|
||||
mutating func basicServer() async {
|
||||
// Given the view model configured for a basic server example.com that only supports password authentication.
|
||||
await setupViewModel()
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the login form.
|
||||
XCTAssertEqual(context.viewState.homeserver, .mockBasicServer, "The homeserver data should should match the new homeserver.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The login form should be shown.")
|
||||
#expect(context.viewState.homeserver == .mockBasicServer,
|
||||
"The homeserver data should should match the new homeserver.")
|
||||
#expect(context.viewState.loginMode == .password,
|
||||
"The login form should be shown.")
|
||||
}
|
||||
|
||||
func testUsernameWithEmptyPassword() async {
|
||||
@Test
|
||||
mutating func usernameWithEmptyPassword() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
#expect(context.password.isEmpty,
|
||||
"The initial value for the password should be empty.")
|
||||
#expect(context.username.isEmpty,
|
||||
"The initial value for the username should be empty.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be invalid.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should be blocked for submission.")
|
||||
|
||||
// When entering a username without a password.
|
||||
context.username = "bob"
|
||||
context.password = ""
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be invalid.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testEmptyUsernameWithPassword() async {
|
||||
@Test
|
||||
mutating func emptyUsernameWithPassword() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
#expect(context.password.isEmpty,
|
||||
"The initial value for the password should be empty.")
|
||||
#expect(context.username.isEmpty,
|
||||
"The initial value for the username should be empty.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be invalid.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should be blocked for submission.")
|
||||
|
||||
// When entering a password without a username.
|
||||
context.username = ""
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered invalid.
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be invalid.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should be blocked for submission.")
|
||||
}
|
||||
|
||||
func testValidCredentials() async {
|
||||
@Test
|
||||
mutating func validCredentials() async {
|
||||
// Given a form with an empty username and password.
|
||||
await setupViewModel()
|
||||
XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.")
|
||||
XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.")
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should be blocked for submission.")
|
||||
#expect(context.password.isEmpty,
|
||||
"The initial value for the password should be empty.")
|
||||
#expect(context.username.isEmpty,
|
||||
"The initial value for the username should be empty.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be invalid.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should be blocked for submission.")
|
||||
|
||||
// When entering a username and an 8-character password.
|
||||
context.username = "bob"
|
||||
context.password = "12345678"
|
||||
|
||||
// Then the credentials should be considered valid.
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
#expect(context.viewState.hasValidCredentials,
|
||||
"The credentials should be valid when the username and password are valid.")
|
||||
#expect(context.viewState.canSubmit,
|
||||
"The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testLoadingServerWithoutPassword() async throws {
|
||||
@Test
|
||||
mutating func loadingServerWithoutPassword() async throws {
|
||||
// Given a form with valid credentials.
|
||||
await setupViewModel()
|
||||
context.username = "@bob:example.com"
|
||||
XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be not be valid without a password.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should not be submittable.")
|
||||
#expect(!context.viewState.hasValidCredentials,
|
||||
"The credentials should be not be valid without a password.")
|
||||
#expect(!context.viewState.isLoading,
|
||||
"The view shouldn't start in a loading state.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should not be submittable.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.isLoading), transitionValues: [true, false])
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.isLoading),
|
||||
transitionValues: [true, false])
|
||||
context.send(viewAction: .parseUsername)
|
||||
|
||||
// Then the view state should represent the loading but never allow submitting to occur.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The form should still not be submittable.")
|
||||
#expect(!context.viewState.isLoading,
|
||||
"The view should be back in a loaded state.")
|
||||
#expect(!context.viewState.canSubmit,
|
||||
"The form should still not be submittable.")
|
||||
}
|
||||
|
||||
func testLoadingServerWithPasswordEntered() async throws {
|
||||
@Test
|
||||
mutating func loadingServerWithPasswordEntered() async throws {
|
||||
// Given a form with valid credentials.
|
||||
await setupViewModel()
|
||||
context.username = "@bob:example.com"
|
||||
context.password = "12345678"
|
||||
XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid.")
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view shouldn't start in a loading state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
#expect(context.viewState.hasValidCredentials,
|
||||
"The credentials should be valid.")
|
||||
#expect(!context.viewState.isLoading,
|
||||
"The view shouldn't start in a loading state.")
|
||||
#expect(context.viewState.canSubmit,
|
||||
"The form should be ready to submit.")
|
||||
|
||||
// When updating the view model whilst loading a homeserver.
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.canSubmit), transitionValues: [false, true])
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.canSubmit),
|
||||
transitionValues: [false, true])
|
||||
context.send(viewAction: .parseUsername)
|
||||
|
||||
// Then the view should be blocked from submitting while loading and then become unblocked again.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertFalse(context.viewState.isLoading, "The view should be back in a loaded state.")
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The form should be ready to submit.")
|
||||
#expect(!context.viewState.isLoading,
|
||||
"The view should be back in a loaded state.")
|
||||
#expect(context.viewState.canSubmit,
|
||||
"The form should be ready to submit.")
|
||||
}
|
||||
|
||||
func testOIDCServer() async throws {
|
||||
@Test
|
||||
mutating func oidcServer() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
|
||||
// When entering a username for a user on a homeserver with OIDC.
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isConfiguredForOIDC }
|
||||
let deferred = deferFulfillment(viewModel.actions) {
|
||||
$0.isConfiguredForOIDC
|
||||
}
|
||||
context.username = "@bob:company.com"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated with the homeserver and show the OIDC button.
|
||||
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The OIDC button should be shown.")
|
||||
#expect(context.viewState.loginMode.supportsOIDCFlow,
|
||||
"The OIDC button should be shown.")
|
||||
}
|
||||
|
||||
func testUnsupportedServer() async throws {
|
||||
@Test
|
||||
mutating func unsupportedServer() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.")
|
||||
#expect(context.alertInfo == nil,
|
||||
"There shouldn't be an alert when the screen loads.")
|
||||
|
||||
// When entering a username for an unsupported homeserver.
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) {
|
||||
$0 != nil
|
||||
}
|
||||
context.username = "@bob:server.net"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated to show an alert.
|
||||
XCTAssertEqual(context.alertInfo?.id, .unknown, "An alert should be shown to the user.")
|
||||
#expect(context.alertInfo?.id == .unknown,
|
||||
"An alert should be shown to the user.")
|
||||
}
|
||||
|
||||
func testElementProRequired() async throws {
|
||||
@Test
|
||||
mutating func elementProRequired() async throws {
|
||||
// Given the screen configured for matrix.org
|
||||
await setupViewModel()
|
||||
XCTAssertNil(context.alertInfo, "There shouldn't be an alert when the screen loads.")
|
||||
#expect(context.alertInfo == nil,
|
||||
"There shouldn't be an alert when the screen loads.")
|
||||
|
||||
// When entering a username for an unsupported homeserver.
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil }
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) {
|
||||
$0 != nil
|
||||
}
|
||||
context.username = "@bob:secure.gov"
|
||||
context.send(viewAction: .parseUsername)
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the view state should be updated to show an alert.
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProAlert, "An alert should be shown to the user.")
|
||||
#expect(context.alertInfo?.id == .elementProAlert,
|
||||
"An alert should be shown to the user.")
|
||||
}
|
||||
|
||||
func testLoginHint() async {
|
||||
@Test
|
||||
mutating func loginHint() async {
|
||||
await setupViewModel(loginHint: "")
|
||||
XCTAssertEqual(context.username, "")
|
||||
#expect(context.username == "")
|
||||
|
||||
await setupViewModel(loginHint: "alice")
|
||||
XCTAssertEqual(context.username, "alice")
|
||||
#expect(context.username == "alice")
|
||||
|
||||
await setupViewModel(loginHint: "mxid:@alice:example.com")
|
||||
XCTAssertEqual(context.username, "@alice:example.com")
|
||||
#expect(context.username == "@alice:example.com")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
|
||||
private mutating func setupViewModel(homeserverAddress: String = "example.com", loginHint: String? = nil) async {
|
||||
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
@@ -181,8 +235,9 @@ class LoginScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appHooks: AppHooks())
|
||||
|
||||
guard case .success = await service.configure(for: homeserverAddress, flow: .login) else {
|
||||
XCTFail("A valid server should be configured for the test.")
|
||||
guard case .success = await service
|
||||
.configure(for: homeserverAddress, flow: .login) else {
|
||||
Issue.record("A valid server should be configured for the test.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,23 +7,25 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ManageRoomMemberSheetViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct ManageRoomMemberSheetViewModelTests {
|
||||
private var viewModel: ManageRoomMemberSheetViewModel!
|
||||
private var context: ManageRoomMemberSheetViewModel.Context! {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testKick() async throws {
|
||||
@Test
|
||||
mutating func kick() async throws {
|
||||
let testReason = "Kick Test"
|
||||
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
|
||||
let expectation = XCTestExpectation(description: "Kick member")
|
||||
var kickCalled = false
|
||||
roomProxy.kickUserReasonClosure = { userID, reason in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID)
|
||||
XCTAssertEqual(reason, testReason)
|
||||
kickCalled = true
|
||||
#expect(userID == RoomMemberProxyMock.mockAlice.userID)
|
||||
#expect(reason == testReason)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
@@ -43,18 +45,19 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
|
||||
|
||||
context.alertInfo?.textFields?[0].text.wrappedValue = testReason
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredAction.fulfill()
|
||||
#expect(kickCalled)
|
||||
}
|
||||
|
||||
func testBan() async throws {
|
||||
@Test
|
||||
mutating func ban() async throws {
|
||||
let testReason = "Ban Test"
|
||||
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
|
||||
let expectation = XCTestExpectation(description: "Ban member")
|
||||
var banCalled = false
|
||||
roomProxy.banUserReasonClosure = { userID, reason in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID)
|
||||
XCTAssertEqual(reason, testReason)
|
||||
banCalled = true
|
||||
#expect(userID == RoomMemberProxyMock.mockAlice.userID)
|
||||
#expect(reason == testReason)
|
||||
return .success(())
|
||||
}
|
||||
|
||||
@@ -74,11 +77,12 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
|
||||
}
|
||||
context.alertInfo?.textFields?[0].text.wrappedValue = testReason
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
await fulfillment(of: [expectation])
|
||||
try await deferredAction.fulfill()
|
||||
#expect(banCalled)
|
||||
}
|
||||
|
||||
func testDisplayDetails() async throws {
|
||||
@Test
|
||||
mutating func displayDetails() async throws {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
|
||||
viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)),
|
||||
permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel),
|
||||
@@ -92,6 +96,6 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
|
||||
}
|
||||
context.send(viewAction: .displayDetails)
|
||||
try await deferredAction.fulfill()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,57 +8,62 @@
|
||||
|
||||
import CoreLocation
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class MapTilerURLBuilderTests: XCTestCase {
|
||||
@Suite
|
||||
struct MapTilerURLBuilderTests {
|
||||
private static let baseURL: URL = "http://www.foo.com"
|
||||
private static let apiKey = "some_key"
|
||||
private static let lightStyleID = "9bc819c8-e627-474a-a348-ec144fe3d810"
|
||||
private static let darkStyleID = "dea61faf-292b-4774-9660-58fcef89a7f3"
|
||||
|
||||
var builder: MapTilerURLBuilderProtocol!
|
||||
var builder: MapTilerURLBuilderProtocol
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
builder = MapTilerConfiguration(baseURL: Self.baseURL,
|
||||
apiKey: Self.apiKey,
|
||||
lightStyleID: Self.lightStyleID,
|
||||
darkStyleID: Self.darkStyleID)
|
||||
}
|
||||
|
||||
func testStaticMapBuilder() {
|
||||
|
||||
@Test
|
||||
func staticMapBuilder() {
|
||||
let url = builder.staticMapTileImageURL(for: .light,
|
||||
coordinates: .init(latitude: 1, longitude: 2),
|
||||
zoomLevel: 5,
|
||||
size: .init(width: 300, height: 200),
|
||||
attribution: .hidden)
|
||||
|
||||
|
||||
let expectedURL: URL = "http://www.foo.com/9bc819c8-e627-474a-a348-ec144fe3d810/static/2.000000,1.000000,5.000000/300x200@2x.png?key=some_key&attribution=false"
|
||||
XCTAssertEqual(url, expectedURL)
|
||||
#expect(url == expectedURL)
|
||||
}
|
||||
|
||||
func testStaticMapBuilderWithAttribution() {
|
||||
|
||||
@Test
|
||||
func staticMapBuilderWithAttribution() {
|
||||
let url = builder.staticMapTileImageURL(for: .dark,
|
||||
coordinates: .init(latitude: 1, longitude: 2),
|
||||
zoomLevel: 5,
|
||||
size: .init(width: 300, height: 200),
|
||||
attribution: .topLeft)
|
||||
|
||||
|
||||
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/static/2.000000,1.000000,5.000000/300x200@2x.png?key=some_key&attribution=topleft"
|
||||
XCTAssertEqual(url, expectedURL)
|
||||
}
|
||||
|
||||
func testDynamicMapBuilder() {
|
||||
let url = builder.interactiveMapURL(for: .dark)
|
||||
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key"
|
||||
XCTAssertEqual(url, expectedURL)
|
||||
#expect(url == expectedURL)
|
||||
}
|
||||
|
||||
func testNilAPIKey() {
|
||||
@Test
|
||||
func dynamicMapBuilder() {
|
||||
let url = builder.interactiveMapURL(for: .dark)
|
||||
let expectedURL: URL = "http://www.foo.com/dea61faf-292b-4774-9660-58fcef89a7f3/style.json?key=some_key"
|
||||
#expect(url == expectedURL)
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func nilAPIKey() {
|
||||
let configuration = MapTilerConfiguration(baseURL: Self.baseURL,
|
||||
apiKey: nil,
|
||||
lightStyleID: Self.lightStyleID,
|
||||
darkStyleID: Self.darkStyleID)
|
||||
XCTAssertFalse(configuration.isEnabled)
|
||||
#expect(!configuration.isEnabled)
|
||||
|
||||
builder = configuration
|
||||
|
||||
@@ -67,9 +72,9 @@ final class MapTilerURLBuilderTests: XCTestCase {
|
||||
zoomLevel: 5,
|
||||
size: .init(width: 300, height: 200),
|
||||
attribution: .topLeft)
|
||||
XCTAssertNil(staticMapURL)
|
||||
#expect(staticMapURL == nil)
|
||||
|
||||
let dynamicMapURL = builder.interactiveMapURL(for: .light)
|
||||
XCTAssertNil(dynamicMapURL)
|
||||
#expect(dynamicMapURL == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,65 +8,71 @@
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class MatrixEntityRegexTests: XCTestCase {
|
||||
func testHomeserver() {
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixHomeserver("matrix.org"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixHomeserver("MATRIX.ORG"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixHomeserver("matrix?.org"))
|
||||
}
|
||||
|
||||
func testUserID() {
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("username:example.com"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com"))
|
||||
@Suite
|
||||
struct MatrixEntityRegexTests {
|
||||
@Test
|
||||
func homeserver() {
|
||||
#expect(MatrixEntityRegex.isMatrixHomeserver("matrix.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixHomeserver("MATRIX.ORG"))
|
||||
#expect(!MatrixEntityRegex.isMatrixHomeserver("matrix?.org"))
|
||||
}
|
||||
|
||||
func testRoomAlias() {
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org"))
|
||||
@Test
|
||||
func userID() {
|
||||
#expect(MatrixEntityRegex.isMatrixUserIdentifier("@username:example.com"))
|
||||
#expect(!MatrixEntityRegex.isMatrixUserIdentifier("username:example.com"))
|
||||
#expect(!MatrixEntityRegex.isMatrixUserIdentifier("@username.example.com"))
|
||||
}
|
||||
|
||||
func testMatrixURI() {
|
||||
@Test
|
||||
func roomAlias() {
|
||||
#expect(MatrixEntityRegex.isMatrixRoomAlias("#element-ios:matrix.org"))
|
||||
#expect(!MatrixEntityRegex.isMatrixRoomAlias("element-ios:matrix.org"))
|
||||
#expect(!MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func matrixURI() {
|
||||
// Users
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org?action=chat"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org?action=chat"))
|
||||
|
||||
// Room ID
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com?via=elsewhere.ca"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net?via=elsewhere.ca&via=other.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com?via=elsewhere.ca"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net?via=elsewhere.ca&via=other.org"))
|
||||
|
||||
// Room Alias
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/general:matrix.org"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/123_room:chat.myserver.net"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:r/general:matrix.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:r/123_room:chat.myserver.net"))
|
||||
|
||||
// Event
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org/e/event"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com/e/message?via=elsewhere.ca"))
|
||||
XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net/e/1234?via=elsewhere.ca&via=other.org"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org/e/event"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com/e/message?via=elsewhere.ca"))
|
||||
#expect(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net/e/1234?via=elsewhere.ca&via=other.org"))
|
||||
|
||||
// Inline
|
||||
let string = "Hello matrix:u/alice:example.org how are you?"
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("Hello matrix:u/alice:example.org how are you?"))
|
||||
XCTAssertEqual(MatrixEntityRegex.uriRegex.matches(in: string).count, 1)
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("Hello matrix:u/alice:example.org how are you?"))
|
||||
#expect(MatrixEntityRegex.uriRegex.matches(in: string).count == 1)
|
||||
|
||||
// Invalid
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://@alice:example.org"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://!somewhere:example.org"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://#general:matrix.org"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:event/somewhere:example.org/e/event"))
|
||||
XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:e/somewhere:example.org/e/event"))
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("matrix://@alice:example.org"))
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("matrix://!somewhere:example.org"))
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("matrix://#general:matrix.org"))
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("matrix:event/somewhere:example.org/e/event"))
|
||||
#expect(!MatrixEntityRegex.isMatrixURI("matrix:e/somewhere:example.org/e/event"))
|
||||
}
|
||||
|
||||
func testAllUsers() {
|
||||
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room"))
|
||||
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a@rooma"))
|
||||
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a @room a"))
|
||||
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("a @roaom a"))
|
||||
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("@roaom"))
|
||||
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room\n"))
|
||||
@Test
|
||||
func allUsers() {
|
||||
#expect(MatrixEntityRegex.containsMatrixAllUsers("@room"))
|
||||
#expect(MatrixEntityRegex.containsMatrixAllUsers("a@rooma"))
|
||||
#expect(MatrixEntityRegex.containsMatrixAllUsers("a @room a"))
|
||||
#expect(!MatrixEntityRegex.containsMatrixAllUsers("a @roaom a"))
|
||||
#expect(!MatrixEntityRegex.containsMatrixAllUsers("@roaom"))
|
||||
#expect(MatrixEntityRegex.containsMatrixAllUsers("@room\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,37 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class MediaPlayerProviderTests: XCTestCase {
|
||||
private var mediaPlayerProvider: MediaPlayerProvider!
|
||||
@Suite
|
||||
struct MediaPlayerProviderTests {
|
||||
private var mediaPlayerProvider: MediaPlayerProvider
|
||||
|
||||
private let oggMimeType = "audio/ogg"
|
||||
private let someURL = URL.mockMXCAudio
|
||||
private let someOtherURL = URL.mockMXCFile
|
||||
|
||||
override func setUp() async throws {
|
||||
init() async {
|
||||
mediaPlayerProvider = MediaPlayerProvider()
|
||||
}
|
||||
|
||||
func testPlayerStates() {
|
||||
@Test
|
||||
func playerStates() {
|
||||
let audioPlayerStateId = AudioPlayerStateIdentifier.timelineItemIdentifier(.randomEvent)
|
||||
// By default, there should be no player state
|
||||
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
#expect(mediaPlayerProvider.playerState(for: audioPlayerStateId) == nil)
|
||||
|
||||
let audioPlayerState = AudioPlayerState(id: audioPlayerStateId, title: "", duration: 10.0)
|
||||
mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
XCTAssertEqual(audioPlayerState, mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
#expect(audioPlayerState == mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
|
||||
mediaPlayerProvider.unregister(audioPlayerState: audioPlayerState)
|
||||
XCTAssertNil(mediaPlayerProvider.playerState(for: audioPlayerStateId))
|
||||
#expect(mediaPlayerProvider.playerState(for: audioPlayerStateId) == nil)
|
||||
}
|
||||
|
||||
func testDetachAllStates() {
|
||||
@Test
|
||||
func detachAllStates() {
|
||||
let audioPlayer = AudioPlayerMock()
|
||||
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
|
||||
|
||||
@@ -45,17 +48,18 @@ class MediaPlayerProviderTests: XCTestCase {
|
||||
mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
let isAttached = audioPlayerState.isAttached
|
||||
XCTAssertTrue(isAttached)
|
||||
#expect(isAttached)
|
||||
}
|
||||
|
||||
mediaPlayerProvider.detachAllStates(except: nil)
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
let isAttached = audioPlayerState.isAttached
|
||||
XCTAssertFalse(isAttached)
|
||||
#expect(!isAttached)
|
||||
}
|
||||
}
|
||||
|
||||
func testDetachAllStatesWithException() {
|
||||
@Test
|
||||
func detachAllStatesWithException() {
|
||||
let audioPlayer = AudioPlayerMock()
|
||||
audioPlayer.actions = PassthroughSubject<AudioPlayerAction, Never>().eraseToAnyPublisher()
|
||||
|
||||
@@ -64,7 +68,7 @@ class MediaPlayerProviderTests: XCTestCase {
|
||||
mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
|
||||
audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
let isAttached = audioPlayerState.isAttached
|
||||
XCTAssertTrue(isAttached)
|
||||
#expect(isAttached)
|
||||
}
|
||||
|
||||
let exception = audioPlayerStates[1]
|
||||
@@ -72,9 +76,9 @@ class MediaPlayerProviderTests: XCTestCase {
|
||||
for audioPlayerState in audioPlayerStates {
|
||||
let isAttached = audioPlayerState.isAttached
|
||||
if audioPlayerState == exception {
|
||||
XCTAssertTrue(isAttached)
|
||||
#expect(isAttached)
|
||||
} else {
|
||||
XCTAssertFalse(isAttached)
|
||||
#expect(!isAttached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,44 +7,40 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class MediaLoaderTests: XCTestCase {
|
||||
func testMediaRequestCoalescing() async throws {
|
||||
@Suite
|
||||
struct MediaLoaderTests {
|
||||
@Test
|
||||
func mediaRequestCoalescing() async throws {
|
||||
let mediaLoadingClient = ClientSDKMock()
|
||||
mediaLoadingClient.getMediaContentMediaSourceReturnValue = Data()
|
||||
let mediaLoader = MediaLoader(client: mediaLoadingClient)
|
||||
|
||||
let mediaSource = try MediaSourceProxy(url: .mockMXCFile, mimeType: nil)
|
||||
|
||||
do {
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaContentForSource(mediaSource)
|
||||
}
|
||||
|
||||
XCTAssertEqual(mediaLoadingClient.getMediaContentMediaSourceCallsCount, 10)
|
||||
} catch {
|
||||
fatalError()
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaContentForSource(mediaSource)
|
||||
}
|
||||
|
||||
#expect(mediaLoadingClient.getMediaContentMediaSourceCallsCount == 10)
|
||||
}
|
||||
|
||||
func testMediaThumbnailRequestCoalescing() async throws {
|
||||
@Test
|
||||
func mediaThumbnailRequestCoalescing() async throws {
|
||||
let mediaLoadingClient = ClientSDKMock()
|
||||
mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightReturnValue = Data()
|
||||
let mediaLoader = MediaLoader(client: mediaLoadingClient)
|
||||
|
||||
let mediaSource = try MediaSourceProxy(url: .mockMXCImage, mimeType: nil)
|
||||
|
||||
do {
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaThumbnailForSource(mediaSource, width: 100, height: 100)
|
||||
}
|
||||
|
||||
XCTAssertEqual(mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightCallsCount, 10)
|
||||
} catch {
|
||||
fatalError()
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaThumbnailForSource(mediaSource, width: 100, height: 100)
|
||||
}
|
||||
|
||||
#expect(mediaLoadingClient.getMediaThumbnailMediaSourceWidthHeightCallsCount == 10)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,19 +227,19 @@ class MediaUploadPreviewScreenViewModelTests: XCTestCase {
|
||||
private var audioURL: URL {
|
||||
assertResourceURL(filename: "test_audio.mp3")
|
||||
}
|
||||
|
||||
|
||||
private var fileURL: URL {
|
||||
assertResourceURL(filename: "test_pdf.pdf")
|
||||
}
|
||||
|
||||
|
||||
private var imageURL: URL {
|
||||
assertResourceURL(filename: "test_animated_image.gif")
|
||||
}
|
||||
|
||||
|
||||
private var videoURL: URL {
|
||||
assertResourceURL(filename: "landscape_test_video.mov")
|
||||
}
|
||||
|
||||
|
||||
private var badImageURL = URL(filePath: "/home/user/this_file_doesn't_exist.jpg")
|
||||
|
||||
private func assertResourceURL(filename: String) -> URL {
|
||||
|
||||
@@ -109,7 +109,7 @@ final class MediaUploadingPreprocessorTests: XCTestCase {
|
||||
XCTAssertEqual(optimizedVideoInfo.height, 720)
|
||||
XCTAssertEqual(optimizedVideoInfo.duration ?? 0, 30, accuracy: 100)
|
||||
}
|
||||
|
||||
|
||||
func testPortraitMp4VideoProcessing() async {
|
||||
// Allow an increased execution time as we encode the video twice now.
|
||||
executionTimeAllowance = 180
|
||||
|
||||
@@ -8,20 +8,18 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class MessageForwardingScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct MessageForwardingScreenViewModelTests {
|
||||
let forwardingItem = MessageForwardingItem(id: .event(uniqueID: .init("t1"), eventOrTransactionID: .eventID("t1")),
|
||||
roomID: "1",
|
||||
content: .init(noHandle: .init()))
|
||||
var viewModel: MessageForwardingScreenViewModelProtocol!
|
||||
var context: MessageForwardingScreenViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
cancellables.removeAll()
|
||||
|
||||
init() {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) }
|
||||
|
||||
@@ -32,45 +30,44 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertNil(context.viewState.rooms.first { $0.id == forwardingItem.roomID }, "The source room ID shouldn't be shown")
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(context.viewState.rooms.first { $0.id == forwardingItem.roomID } == nil, "The source room ID shouldn't be shown")
|
||||
}
|
||||
|
||||
func testRoomSelection() {
|
||||
@Test
|
||||
mutating func roomSelection() {
|
||||
context.send(viewAction: .selectRoom(roomID: "2"))
|
||||
XCTAssertEqual(context.viewState.selectedRoomID, "2")
|
||||
#expect(context.viewState.selectedRoomID == "2")
|
||||
}
|
||||
|
||||
func testSearching() async throws {
|
||||
let defered = deferFulfillment(context.$viewState) { state in
|
||||
@Test
|
||||
mutating func searching() async throws {
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.rooms.count == 1
|
||||
}
|
||||
|
||||
context.searchQuery = "Second"
|
||||
|
||||
try await defered.fulfill()
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testForwarding() {
|
||||
@Test
|
||||
mutating func forwarding() async throws {
|
||||
context.send(viewAction: .selectRoom(roomID: "2"))
|
||||
XCTAssertEqual(context.viewState.selectedRoomID, "2")
|
||||
#expect(context.viewState.selectedRoomID == "2")
|
||||
|
||||
let expectation = expectation(description: "Wait for confirmation")
|
||||
|
||||
viewModel.actions
|
||||
.sink { action in
|
||||
switch action {
|
||||
case .sent(let roomID):
|
||||
XCTAssertEqual(roomID, "2")
|
||||
expectation.fulfill()
|
||||
default:
|
||||
break
|
||||
}
|
||||
let deferred = deferFulfillment(viewModel.actions) { action in
|
||||
switch action {
|
||||
case .sent(let roomID):
|
||||
return roomID == "2"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
context.send(viewAction: .send)
|
||||
|
||||
waitForExpectations(timeout: 5.0)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class NavigationRootCoordinatorTests: XCTestCase {
|
||||
private var navigationRootCoordinator: NavigationRootCoordinator!
|
||||
@Suite
|
||||
struct NavigationRootCoordinatorTests {
|
||||
private var navigationRootCoordinator: NavigationRootCoordinator
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
navigationRootCoordinator = NavigationRootCoordinator()
|
||||
}
|
||||
|
||||
func testRootChanges() {
|
||||
XCTAssertNil(navigationRootCoordinator.rootCoordinator)
|
||||
@Test
|
||||
func rootChanges() {
|
||||
#expect(navigationRootCoordinator.rootCoordinator == nil)
|
||||
|
||||
let firstRootCoordinator = SomeTestCoordinator()
|
||||
navigationRootCoordinator.setRootCoordinator(firstRootCoordinator)
|
||||
@@ -31,7 +34,8 @@ class NavigationRootCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testOverlay() {
|
||||
@Test
|
||||
func overlay() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationRootCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -44,35 +48,37 @@ class NavigationRootCoordinatorTests: XCTestCase {
|
||||
navigationRootCoordinator.setOverlayCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationRootCoordinator.rootCoordinator)
|
||||
XCTAssertNil(navigationRootCoordinator.overlayCoordinator)
|
||||
#expect(navigationRootCoordinator.overlayCoordinator == nil)
|
||||
}
|
||||
|
||||
// MARK: - Dismissal Callbacks
|
||||
|
||||
func testReplacementDismissalCallbacks() {
|
||||
XCTAssertNil(navigationRootCoordinator.rootCoordinator)
|
||||
@Test
|
||||
func replacementDismissalCallbacks() async {
|
||||
#expect(navigationRootCoordinator.rootCoordinator == nil)
|
||||
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationRootCoordinator.setRootCoordinator(rootCoordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationRootCoordinator.setRootCoordinator(rootCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testOverlayDismissalCallback() {
|
||||
@Test
|
||||
func overlayDismissalCallback() async {
|
||||
let overlayCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setOverlayCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setOverlayCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -80,11 +86,11 @@ class NavigationRootCoordinatorTests: XCTestCase {
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
Issue.record("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
#expect(lhs.id == rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,21 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class NavigationStackCoordinatorTests: XCTestCase {
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
@Suite
|
||||
struct NavigationStackCoordinatorTests {
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
}
|
||||
|
||||
func testRoot() {
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
@Test
|
||||
func root() {
|
||||
#expect(navigationStackCoordinator.rootCoordinator == nil)
|
||||
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
@@ -26,7 +29,8 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
@Test
|
||||
mutating func singleSheet() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -39,10 +43,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator)
|
||||
#expect(navigationStackCoordinator.sheetCoordinator == nil)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
@Test
|
||||
mutating func multipleSheets() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -50,18 +55,19 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
|
||||
let someOtherSheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(someOtherSheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSinglePush() {
|
||||
@Test
|
||||
mutating func singlePush() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -74,10 +80,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator.pop()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testMultiplePushes() {
|
||||
@Test
|
||||
mutating func multiplePushes() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -89,7 +96,7 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
}
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, coordinators.count)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.count == coordinators.count)
|
||||
|
||||
for index in coordinators.indices {
|
||||
assertCoordinatorsEqual(coordinators[index], navigationStackCoordinator.stackCoordinators[index])
|
||||
@@ -98,10 +105,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator.popToRoot()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testRootReplacementDimissesTheRest() {
|
||||
@Test
|
||||
mutating func rootReplacementDimissesTheRest() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
@@ -119,10 +127,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
navigationStackCoordinator.setRootCoordinator(newRootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(newRootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testPushesDontReplaceSheet() {
|
||||
@Test
|
||||
mutating func pushesDontReplaceSheet() {
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
@@ -142,54 +151,57 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
|
||||
// MARK: - Dismissal Callbacks
|
||||
|
||||
func testPopDismissalCallbacks() {
|
||||
@Test
|
||||
mutating func popDismissalCallbacks() async {
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.push(pushedCoordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationStackCoordinator.push(pushedCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.pop()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testPopToRootDismissalCallbacks() {
|
||||
@Test
|
||||
mutating func popToRootDismissalCallbacks() async {
|
||||
navigationStackCoordinator.push(SomeTestCoordinator())
|
||||
navigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.push(coordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationStackCoordinator.push(coordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.popToRoot()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.popToRoot()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallback() {
|
||||
@Test
|
||||
mutating func sheetDismissalCallback() async {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testRootReplacementCallbacks() {
|
||||
@Test
|
||||
mutating func rootReplacementCallbacks() async {
|
||||
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let popExpectation = expectation(description: "Waiting for callback")
|
||||
navigationStackCoordinator.push(SomeTestCoordinator()) {
|
||||
popExpectation.fulfill()
|
||||
await confirmation("Waiting for callback") { confirm in
|
||||
navigationStackCoordinator.push(SomeTestCoordinator()) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -197,11 +209,11 @@ class NavigationStackCoordinatorTests: XCTestCase {
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
Issue.record("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
#expect(lhs.id == rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,22 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class NavigationTabCoordinatorTests: XCTestCase {
|
||||
@Suite
|
||||
struct NavigationTabCoordinatorTests {
|
||||
enum TestTab { case tab, chats, spaces }
|
||||
private var navigationTabCoordinator: NavigationTabCoordinator<TestTab>!
|
||||
private var navigationTabCoordinator: NavigationTabCoordinator<TestTab>
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
navigationTabCoordinator = NavigationTabCoordinator()
|
||||
}
|
||||
|
||||
func testTabs() {
|
||||
XCTAssertTrue(navigationTabCoordinator.tabCoordinators.isEmpty)
|
||||
@Test
|
||||
mutating func tabs() {
|
||||
#expect(navigationTabCoordinator.tabCoordinators.isEmpty)
|
||||
|
||||
let someCoordinator = SomeTestCoordinator()
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: someCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
@@ -34,7 +37,8 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
@Test
|
||||
mutating func singleSheet() {
|
||||
let tabCoordinator = SomeTestCoordinator()
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
|
||||
@@ -47,10 +51,11 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
navigationTabCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
|
||||
XCTAssertNil(navigationTabCoordinator.sheetCoordinator)
|
||||
#expect(navigationTabCoordinator.sheetCoordinator == nil)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
@Test
|
||||
mutating func multipleSheets() {
|
||||
let tabCoordinator = SomeTestCoordinator()
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
|
||||
@@ -67,7 +72,8 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationTabCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testFullScreenCover() {
|
||||
@Test
|
||||
mutating func fullScreenCover() {
|
||||
let tabCoordinator = SomeTestCoordinator()
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
|
||||
@@ -80,10 +86,11 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
|
||||
XCTAssertNil(navigationTabCoordinator.fullScreenCoverCoordinator)
|
||||
#expect(navigationTabCoordinator.fullScreenCoverCoordinator == nil)
|
||||
}
|
||||
|
||||
func testOverlay() {
|
||||
@Test
|
||||
mutating func overlay() {
|
||||
let tabCoordinator = SomeTestCoordinator()
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: tabCoordinator, details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
|
||||
@@ -102,73 +109,77 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
navigationTabCoordinator.setOverlayCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [tabCoordinator])
|
||||
XCTAssertNil(navigationTabCoordinator.overlayCoordinator)
|
||||
#expect(navigationTabCoordinator.overlayCoordinator == nil)
|
||||
}
|
||||
|
||||
// MARK: - Dismissal Callbacks
|
||||
|
||||
func testTabDismissalCallbacks() {
|
||||
@Test
|
||||
mutating func tabDismissalCallbacks() async {
|
||||
let chatsCoordinator = SomeTestCoordinator()
|
||||
let spacesCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
expectation.expectedFulfillmentCount = 2
|
||||
|
||||
navigationTabCoordinator.setTabs([
|
||||
.init(coordinator: chatsCoordinator, details: .init(tag: .chats, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid)) { expectation.fulfill() },
|
||||
.init(coordinator: spacesCoordinator, details: .init(tag: .spaces, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid)) { expectation.fulfill() }
|
||||
])
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
||||
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallback() {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationTabCoordinator.setSheetCoordinator(coordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback", expectedCount: 2) { confirm in
|
||||
navigationTabCoordinator.setTabs([
|
||||
.init(coordinator: chatsCoordinator, details: .init(tag: .chats, title: "Chats", icon: \.chat, selectedIcon: \.chatSolid)) { confirm() },
|
||||
.init(coordinator: spacesCoordinator, details: .init(tag: .spaces, title: "Spaces", icon: \.space, selectedIcon: \.spaceSolid)) { confirm() }
|
||||
])
|
||||
assertCoordinatorsEqual(navigationTabCoordinator.tabCoordinators, [chatsCoordinator, spacesCoordinator])
|
||||
|
||||
navigationTabCoordinator.setTabs([.init(coordinator: SomeTestCoordinator(), details: .init(tag: .tab, title: "Tab", icon: \.help, selectedIcon: \.helpSolid))])
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setSheetCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testFullScreenCoverDismissalCallback() {
|
||||
@Test
|
||||
mutating func sheetDismissalCallback() async {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationTabCoordinator.setSheetCoordinator(coordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testOverlayDismissalCallback() {
|
||||
@Test
|
||||
mutating func fullScreenCoverDismissalCallback() async {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationTabCoordinator.setFullScreenCoverCoordinator(coordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func overlayDismissalCallback() async {
|
||||
let overlayCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
expectation.fulfill()
|
||||
await confirmation("Wait for callback") { confirm in
|
||||
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
confirm()
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setOverlayCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setOverlayCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testOverlayDismissalCallbackWhenChangingMode() {
|
||||
@Test
|
||||
mutating func overlayDismissalCallbackWhenChangingMode() async throws {
|
||||
let overlayCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
expectation.isInverted = true
|
||||
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
expectation.fulfill()
|
||||
try await confirmation("Callback should not be called when just changing mode",
|
||||
expectedCount: 0) { confirmation in
|
||||
navigationTabCoordinator.setOverlayCoordinator(overlayCoordinator) {
|
||||
confirmation()
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setOverlayPresentationMode(.minimized)
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
}
|
||||
|
||||
navigationTabCoordinator.setOverlayPresentationMode(.minimized)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -176,16 +187,16 @@ class NavigationTabCoordinatorTests: XCTestCase {
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
Issue.record("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
#expect(lhs.id == rhs.id)
|
||||
}
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: [CoordinatorProtocol], _ rhs: [CoordinatorProtocol]) {
|
||||
guard lhs.count == rhs.count else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
Issue.record("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
import Dynamic
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
import UserNotifications
|
||||
|
||||
final class NotificationContentBuilderTests: XCTestCase {
|
||||
var notificationContentBuilder: NotificationContentBuilder!
|
||||
var mediaProvider: MediaProviderMock!
|
||||
var notificationContent: UNMutableNotificationContent!
|
||||
@Suite
|
||||
struct NotificationContentBuilderTests {
|
||||
var notificationContentBuilder: NotificationContentBuilder
|
||||
var mediaProvider: MediaProviderMock
|
||||
var notificationContent: UNMutableNotificationContent
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
notificationContent = .init()
|
||||
let stringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
|
||||
destination: .notification)
|
||||
@@ -25,7 +27,8 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
userSession: NSEUserSessionMock(.init()))
|
||||
}
|
||||
|
||||
func testDMMessageNotification() async {
|
||||
@Test
|
||||
mutating func dmMessageNotification() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -40,18 +43,19 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
// Checking if nil without using asObject always fails
|
||||
XCTAssertNil(communicationContext.displayName.asObject)
|
||||
XCTAssertEqual(communicationContext.sender.displayName, "Alice")
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
#expect(communicationContext.displayName.asObject == nil)
|
||||
#expect(communicationContext.sender.displayName == "Alice")
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID == nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
// Remember we remove the @ due to an iOS bug
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.org")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.org")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testDMMessageNotificationWithMention() async {
|
||||
@Test
|
||||
mutating func dmMessageNotificationWithMention() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -68,18 +72,19 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
// Checking if nil without using asObject always fails
|
||||
XCTAssertNil(communicationContext.displayName.asObject)
|
||||
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice"))
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
#expect(communicationContext.displayName.asObject == nil)
|
||||
#expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID == nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
// Remember we remove the @ due to an iOS bug
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.org")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.org")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testDMMessageNotificationWithThread() async {
|
||||
@Test
|
||||
mutating func dmMessageNotificationWithThread() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -96,18 +101,19 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
XCTAssertEqual(communicationContext.displayName, L10n.commonThread)
|
||||
XCTAssertEqual(communicationContext.sender.displayName, "Alice")
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNotNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
#expect(communicationContext.displayName == L10n.commonThread)
|
||||
#expect(communicationContext.sender.displayName == "Alice")
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID != nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
// Remember we remove the @ due to an iOS bug
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.orgthread")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.orgthread")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testDMMessageNotificationWithThreadAndMention() async {
|
||||
@Test
|
||||
mutating func dmMessageNotificationWithThreadAndMention() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!test:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -124,18 +130,19 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
XCTAssertEqual(communicationContext.displayName, L10n.commonThread)
|
||||
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice"))
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNotNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
#expect(communicationContext.displayName == L10n.commonThread)
|
||||
#expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID != nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
// Remember we remove the @ due to an iOS bug
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!test:matrix.orgthread")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.orgthread")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testRoomMessageNotification() async {
|
||||
@Test
|
||||
mutating func roomMessageNotification() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -150,18 +157,19 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
mediaProvider: mediaProvider)
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
|
||||
XCTAssertEqual(communicationContext.displayName, "General")
|
||||
XCTAssertEqual(communicationContext.sender.displayName, "Alice")
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNil(notificationContent.sound)
|
||||
#expect(communicationContext.displayName == "General")
|
||||
#expect(communicationContext.sender.displayName == "Alice")
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID == nil)
|
||||
#expect(notificationContent.sound == nil)
|
||||
// Remember we remove the @ due to an iOS bug
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.org")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.org")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testRoomMessageNotificationWithMention() async {
|
||||
@Test
|
||||
mutating func roomMessageNotificationWithMention() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -177,17 +185,18 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
XCTAssertEqual(communicationContext.displayName, "General")
|
||||
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice"))
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.org")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(communicationContext.displayName == "General")
|
||||
#expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID == nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.org")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testRoomMessageNotificationWithThread() async {
|
||||
@Test
|
||||
mutating func roomMessageNotificationWithThread() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -203,17 +212,18 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
XCTAssertEqual(communicationContext.displayName, L10n.notificationThreadInRoom("General"))
|
||||
XCTAssertEqual(communicationContext.sender.displayName, "Alice")
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNotNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNil(notificationContent.sound)
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.orgthread123")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(communicationContext.displayName == L10n.notificationThreadInRoom("General"))
|
||||
#expect(communicationContext.sender.displayName == "Alice")
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID != nil)
|
||||
#expect(notificationContent.sound == nil)
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.orgthread123")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
|
||||
func testRoomMessageNotificationWithThreadAndMention() async {
|
||||
@Test
|
||||
mutating func roomMessageNotificationWithThreadAndMention() async {
|
||||
let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org",
|
||||
receiverID: "@bob:matrix.org",
|
||||
senderDisplayName: "Alice",
|
||||
@@ -228,13 +238,13 @@ final class NotificationContentBuilderTests: XCTestCase {
|
||||
notificationItem: notificationItem,
|
||||
mediaProvider: mediaProvider)
|
||||
let communicationContext = Dynamic(notificationContent, memberName: "communicationContext")
|
||||
XCTAssertEqual(communicationContext.displayName, L10n.notificationThreadInRoom("General"))
|
||||
XCTAssertEqual(communicationContext.sender.displayName, L10n.notificationSenderMentionReply("Alice"))
|
||||
XCTAssertEqual(notificationContent.body, "Hello world!")
|
||||
XCTAssertEqual(notificationContent.categoryIdentifier, NotificationConstants.Category.message)
|
||||
XCTAssertNotNil(notificationContent.threadRootEventID)
|
||||
XCTAssertNotNil(notificationContent.sound)
|
||||
XCTAssertEqual(notificationContent.threadIdentifier, "bob:matrix.org!testroom:matrix.orgthread123")
|
||||
XCTAssertEqual(notificationContent.attachments, [])
|
||||
#expect(communicationContext.displayName == L10n.notificationThreadInRoom("General"))
|
||||
#expect(communicationContext.sender.displayName == L10n.notificationSenderMentionReply("Alice"))
|
||||
#expect(notificationContent.body == "Hello world!")
|
||||
#expect(notificationContent.categoryIdentifier == NotificationConstants.Category.message)
|
||||
#expect(notificationContent.threadRootEventID != nil)
|
||||
#expect(notificationContent.sound != nil)
|
||||
#expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.orgthread123")
|
||||
#expect(notificationContent.attachments == [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,46 +7,50 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
/// Just for API sanity checking, they're already properly tested in the SDK/Ruma
|
||||
class PermalinkTests: XCTestCase {
|
||||
func testUserIdentifierPermalink() {
|
||||
@Suite
|
||||
struct PermalinkTests {
|
||||
@Test
|
||||
func userIdentifierPermalink() throws {
|
||||
let invalidUserId = "This1sN0tV4lid!@#$%^&*()"
|
||||
XCTAssertNil(try? matrixToUserPermalink(userId: invalidUserId))
|
||||
#expect(throws: (any Error).self) { try matrixToUserPermalink(userId: invalidUserId) }
|
||||
|
||||
let validUserId = "@abcdefghijklmnopqrstuvwxyz1234567890._-=/:matrix.org"
|
||||
XCTAssertEqual(try? matrixToUserPermalink(userId: validUserId), .some("https://matrix.to/#/@abcdefghijklmnopqrstuvwxyz1234567890._-=%2F:matrix.org"))
|
||||
#expect(try matrixToUserPermalink(userId: validUserId) == "https://matrix.to/#/@abcdefghijklmnopqrstuvwxyz1234567890._-=%2F:matrix.org")
|
||||
}
|
||||
|
||||
func testPermalinkDetection() {
|
||||
@Test
|
||||
func permalinkDetection() {
|
||||
var url: URL = "https://www.matrix.org"
|
||||
XCTAssertNil(parseMatrixEntityFrom(uri: url.absoluteString))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) == nil)
|
||||
|
||||
url = "https://matrix.to/#/@bob:matrix.org?via=matrix.org"
|
||||
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString),
|
||||
MatrixEntity(id: .user(id: "@bob:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
|
||||
MatrixEntity(id: .user(id: "@bob:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
|
||||
url = "https://matrix.to/#/!roomidentifier:matrix.org?via=matrix.org"
|
||||
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString),
|
||||
MatrixEntity(id: .room(id: "!roomidentifier:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
|
||||
MatrixEntity(id: .room(id: "!roomidentifier:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
|
||||
url = "https://matrix.to/#/%23roomalias:matrix.org?via=matrix.org"
|
||||
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString),
|
||||
MatrixEntity(id: .roomAlias(alias: "#roomalias:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
|
||||
MatrixEntity(id: .roomAlias(alias: "#roomalias:matrix.org"),
|
||||
via: ["matrix.org"]))
|
||||
|
||||
url = "https://matrix.to/#/!roomidentifier:matrix.org/$eventidentifier?via=matrix.org"
|
||||
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString),
|
||||
MatrixEntity(id: .eventOnRoomId(roomId: "!roomidentifier:matrix.org", eventId: "$eventidentifier"),
|
||||
via: ["matrix.org"]))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
|
||||
MatrixEntity(id: .eventOnRoomId(roomId: "!roomidentifier:matrix.org", eventId: "$eventidentifier"),
|
||||
via: ["matrix.org"]))
|
||||
|
||||
url = "https://matrix.to/#/#roomalias:matrix.org/$eventidentifier?via=matrix.org"
|
||||
XCTAssertEqual(parseMatrixEntityFrom(uri: url.absoluteString),
|
||||
MatrixEntity(id: .eventOnRoomAlias(alias: "#roomalias:matrix.org", eventId: "$eventidentifier"),
|
||||
via: ["matrix.org"]))
|
||||
#expect(parseMatrixEntityFrom(uri: url.absoluteString) ==
|
||||
MatrixEntity(id: .eventOnRoomAlias(alias: "#roomalias:matrix.org", eventId: "$eventidentifier"),
|
||||
via: ["matrix.org"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class PillContextTests: XCTestCase {
|
||||
func testUser() async {
|
||||
@Suite
|
||||
struct PillContextTests {
|
||||
@Test
|
||||
func user() async {
|
||||
let id = "@test:matrix.org"
|
||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
@@ -30,19 +33,20 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertEqual(context.viewState.displayText, id)
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(context.viewState.displayText == id)
|
||||
|
||||
let name = "Mr. Test"
|
||||
let avatarURL = URL(string: "https://test.jpg")
|
||||
subject.send([RoomMemberProxyMock(with: .init(userID: id, displayName: name, avatarURL: avatarURL, membership: .join))])
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertEqual(context.viewState.displayText, "@\(name)")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(context.viewState.displayText == "@\(name)")
|
||||
}
|
||||
|
||||
func testOwnUser() {
|
||||
@Test
|
||||
func ownUser() {
|
||||
let id = "@test:matrix.org"
|
||||
let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id))
|
||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||
@@ -60,10 +64,11 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertTrue(context.viewState.isOwnMention)
|
||||
#expect(context.viewState.isOwnMention)
|
||||
}
|
||||
|
||||
func testAllUsers() {
|
||||
@Test
|
||||
func allUsers() {
|
||||
let avatarURL = URL(string: "https://matrix.jpg")
|
||||
let id = "test_room"
|
||||
let displayName = "Test"
|
||||
@@ -83,11 +88,12 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertTrue(context.viewState.isOwnMention)
|
||||
XCTAssertEqual(context.viewState.displayText, PillUtilities.atRoom)
|
||||
#expect(context.viewState.isOwnMention)
|
||||
#expect(context.viewState.displayText == PillUtilities.atRoom)
|
||||
}
|
||||
|
||||
func testRoomIDMention() {
|
||||
@Test
|
||||
func roomIDMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
let clientMock = ClientProxyMock(.init())
|
||||
@@ -106,12 +112,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "#Foundation 🔭🪐🌌")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "#Foundation 🔭🪐🌌")
|
||||
}
|
||||
|
||||
func testRoomIDMentionMissingRoom() {
|
||||
@Test
|
||||
func roomIDMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -128,12 +135,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "1")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "1")
|
||||
}
|
||||
|
||||
func testRoomAliasMention() {
|
||||
@Test
|
||||
func roomAliasMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -154,12 +162,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "#Foundation and Empire")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "#Foundation and Empire")
|
||||
}
|
||||
|
||||
func testRoomAliasMentionMissingRoom() {
|
||||
@Test
|
||||
func roomAliasMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -176,12 +185,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "#foundation-and-empire:matrix.org")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "#foundation-and-empire:matrix.org")
|
||||
}
|
||||
|
||||
func testEventOnRoomIDMention() {
|
||||
@Test
|
||||
func eventOnRoomIDMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -200,12 +210,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "💬 > #Foundation 🔭🪐🌌")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "💬 > #Foundation 🔭🪐🌌")
|
||||
}
|
||||
|
||||
func testEventOnRoomIDMentionMissingRoom() {
|
||||
@Test
|
||||
func eventOnRoomIDMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -222,12 +233,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "💬 > 1")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "💬 > 1")
|
||||
}
|
||||
|
||||
func testEventOnRoomAliasMention() {
|
||||
@Test
|
||||
func eventOnRoomAliasMention() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -248,12 +260,13 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "💬 > #Foundation and Empire")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "💬 > #Foundation and Empire")
|
||||
}
|
||||
|
||||
func testEventOnRoomAliasMentionMissingRoom() {
|
||||
@Test
|
||||
func eventOnRoomAliasMentionMissingRoom() {
|
||||
let proxyMock = JoinedRoomProxyMock(.init())
|
||||
let mockController = MockTimelineController()
|
||||
mockController.roomProxy = proxyMock
|
||||
@@ -270,8 +283,8 @@ class PillContextTests: XCTestCase {
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body)))
|
||||
|
||||
XCTAssertFalse(context.viewState.isOwnMention)
|
||||
XCTAssertFalse(context.viewState.isUndefined)
|
||||
XCTAssertEqual(context.viewState.displayText, "💬 > #foundation-and-empire:matrix.org")
|
||||
#expect(!context.viewState.isOwnMention)
|
||||
#expect(!context.viewState.isUndefined)
|
||||
#expect(context.viewState.displayText == "💬 > #foundation-and-empire:matrix.org")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,119 +7,126 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class PinnedEventsBannerStateTests: XCTestCase {
|
||||
func testEmpty() {
|
||||
@Suite
|
||||
struct PinnedEventsBannerStateTests {
|
||||
@Test
|
||||
func empty() {
|
||||
var state = PinnedEventsBannerState.loading(numbersOfEvents: 0)
|
||||
XCTAssertTrue(state.isEmpty)
|
||||
#expect(state.isEmpty)
|
||||
|
||||
state = .loaded(state: .init())
|
||||
XCTAssertTrue(state.isEmpty)
|
||||
#expect(state.isEmpty)
|
||||
}
|
||||
|
||||
func testLoading() {
|
||||
@Test
|
||||
func loading() {
|
||||
let originalState = PinnedEventsBannerState.loading(numbersOfEvents: 5)
|
||||
|
||||
var state = originalState
|
||||
// This should not affect the state when loading
|
||||
state.previousPin()
|
||||
XCTAssertEqual(state, originalState)
|
||||
#expect(state == originalState)
|
||||
|
||||
XCTAssertTrue(state.isLoading)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
XCTAssertNil(state.selectedPinnedEventID)
|
||||
XCTAssertEqual(state.displayedMessage.string, L10n.screenRoomPinnedBannerLoadingDescription)
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 4)
|
||||
XCTAssertEqual(state.count, 5)
|
||||
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5)))
|
||||
#expect(state.isLoading)
|
||||
#expect(!state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == nil)
|
||||
#expect(state.displayedMessage.string == L10n.screenRoomPinnedBannerLoadingDescription)
|
||||
#expect(state.selectedPinnedIndex == 4)
|
||||
#expect(state.count == 5)
|
||||
#expect(state.bannerIndicatorDescription.string == L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(5, 5)))
|
||||
}
|
||||
|
||||
func testLoadingToLoaded() {
|
||||
@Test
|
||||
func loadingToLoaded() {
|
||||
var state = PinnedEventsBannerState.loading(numbersOfEvents: 2)
|
||||
XCTAssertTrue(state.isLoading)
|
||||
#expect(state.isLoading)
|
||||
state.setPinnedEventContents(["1": "test1", "2": "test2"])
|
||||
XCTAssertEqual(state, .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2")))
|
||||
XCTAssertFalse(state.isLoading)
|
||||
#expect(state == .loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2")))
|
||||
#expect(!state.isLoading)
|
||||
}
|
||||
|
||||
func testLoaded() {
|
||||
@Test
|
||||
func loaded() {
|
||||
let state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2"], selectedPinnedEventID: "2"))
|
||||
XCTAssertFalse(state.isLoading)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||
XCTAssertEqual(state.count, 2)
|
||||
XCTAssertEqual(state.bannerIndicatorDescription.string, L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(2, 2)))
|
||||
#expect(!state.isLoading)
|
||||
#expect(!state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == "2")
|
||||
#expect(state.displayedMessage.string == "test2")
|
||||
#expect(state.selectedPinnedIndex == 1)
|
||||
#expect(state.count == 2)
|
||||
#expect(state.bannerIndicatorDescription.string == L10n.screenRoomPinnedBannerIndicatorDescription(L10n.screenRoomPinnedBannerIndicator(2, 2)))
|
||||
}
|
||||
|
||||
func testPreviousPin() {
|
||||
@Test
|
||||
func previousPin() {
|
||||
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3"], selectedPinnedEventID: "1"))
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "1")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 0)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test1")
|
||||
#expect(state.selectedPinnedEventID == "1")
|
||||
#expect(state.selectedPinnedIndex == 0)
|
||||
#expect(state.displayedMessage.string == "test1")
|
||||
|
||||
state.previousPin()
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "3")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 2)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test3")
|
||||
#expect(state.selectedPinnedEventID == "3")
|
||||
#expect(state.selectedPinnedIndex == 2)
|
||||
#expect(state.displayedMessage.string == "test3")
|
||||
|
||||
state.previousPin()
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||
#expect(state.selectedPinnedEventID == "2")
|
||||
#expect(state.selectedPinnedIndex == 1)
|
||||
#expect(state.displayedMessage.string == "test2")
|
||||
}
|
||||
|
||||
func testSetContent() {
|
||||
@Test
|
||||
func setContent() {
|
||||
var state = PinnedEventsBannerState.loaded(state: .init(pinnedEventContents: ["1": "test1", "2": "test2", "3": "test3", "4": "test4"], selectedPinnedEventID: "2"))
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "2")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test2")
|
||||
XCTAssertEqual(state.count, 4)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == "2")
|
||||
#expect(state.selectedPinnedIndex == 1)
|
||||
#expect(state.displayedMessage.string == "test2")
|
||||
#expect(state.count == 4)
|
||||
#expect(!state.isEmpty)
|
||||
|
||||
// let's remove the selected item
|
||||
state.setPinnedEventContents(["1": "test1", "3": "test3", "4": "test4"])
|
||||
// new selected item is the new latest
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 2)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||
XCTAssertEqual(state.count, 3)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == "4")
|
||||
#expect(state.selectedPinnedIndex == 2)
|
||||
#expect(state.displayedMessage.string == "test4")
|
||||
#expect(state.count == 3)
|
||||
#expect(!state.isEmpty)
|
||||
|
||||
// let's add a new item at the top
|
||||
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4"])
|
||||
// selected item doesn't change
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||
#expect(state.selectedPinnedEventID == "4")
|
||||
// but the index is updated
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 3)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||
XCTAssertEqual(state.count, 4)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
#expect(state.selectedPinnedIndex == 3)
|
||||
#expect(state.displayedMessage.string == "test4")
|
||||
#expect(state.count == 4)
|
||||
#expect(!state.isEmpty)
|
||||
|
||||
// let's add a new item at the bottom
|
||||
state.setPinnedEventContents(["0": "test0", "1": "test1", "3": "test3", "4": "test4", "5": "test5"])
|
||||
// selected item doesn't change
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "4")
|
||||
#expect(state.selectedPinnedEventID == "4")
|
||||
// and index stays the same
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 3)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test4")
|
||||
XCTAssertEqual(state.count, 5)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
#expect(state.selectedPinnedIndex == 3)
|
||||
#expect(state.displayedMessage.string == "test4")
|
||||
#expect(state.count == 5)
|
||||
#expect(!state.isEmpty)
|
||||
|
||||
// set to tempty
|
||||
state.setPinnedEventContents([:])
|
||||
XCTAssertTrue(state.isEmpty)
|
||||
XCTAssertNil(state.selectedPinnedEventID)
|
||||
#expect(state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == nil)
|
||||
|
||||
// set to one item
|
||||
state.setPinnedEventContents(["6": "test6", "7": "test7"])
|
||||
XCTAssertEqual(state.selectedPinnedEventID, "7")
|
||||
XCTAssertEqual(state.selectedPinnedIndex, 1)
|
||||
XCTAssertEqual(state.displayedMessage.string, "test7")
|
||||
XCTAssertEqual(state.count, 2)
|
||||
XCTAssertFalse(state.isEmpty)
|
||||
#expect(state.selectedPinnedEventID == "7")
|
||||
#expect(state.selectedPinnedIndex == 1)
|
||||
#expect(state.displayedMessage.string == "test7")
|
||||
#expect(state.count == 2)
|
||||
#expect(!state.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,165 +7,186 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class PollFormScreenViewModelTests: XCTestCase {
|
||||
let timelineProxy = TimelineProxyMock(.init())
|
||||
@Suite
|
||||
struct PollFormScreenViewModelTests {
|
||||
private let timelineProxy = TimelineProxyMock(.init())
|
||||
|
||||
var viewModel: PollFormScreenViewModelProtocol!
|
||||
var context: PollFormScreenViewModelType.Context {
|
||||
private var viewModel: PollFormScreenViewModelProtocol!
|
||||
private var context: PollFormScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testNewPollInitialState() async throws {
|
||||
@Test
|
||||
mutating func newPollInitialState() async throws {
|
||||
setupViewModel()
|
||||
|
||||
XCTAssertEqual(context.options.count, 2)
|
||||
XCTAssertTrue(context.options.allSatisfy(\.text.isEmpty))
|
||||
XCTAssertTrue(context.question.isEmpty)
|
||||
XCTAssertTrue(context.viewState.isSubmitButtonDisabled)
|
||||
XCTAssertFalse(context.viewState.bindings.isUndisclosed)
|
||||
#expect(context.options.count == 2)
|
||||
// This due to a bug in Swift testing that raises an error when allSatisfy is used in an #expect
|
||||
let isEmpty = context.options.allSatisfy(\.text.isEmpty)
|
||||
#expect(isEmpty)
|
||||
#expect(context.question.isEmpty)
|
||||
#expect(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.bindings.isUndisclosed)
|
||||
|
||||
// Cancellation should work without confirmation
|
||||
let deferred = deferFulfillment(viewModel.actions) { _ in true }
|
||||
context.send(viewAction: .cancel)
|
||||
let action = try await deferred.fulfill()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertEqual(action, .close)
|
||||
#expect(context.alertInfo == nil)
|
||||
#expect(action == .close)
|
||||
}
|
||||
|
||||
func testEditPollInitialState() async throws {
|
||||
@Test
|
||||
mutating func editPollInitialState() async throws {
|
||||
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
|
||||
|
||||
XCTAssertEqual(context.options.count, 3)
|
||||
XCTAssertTrue(context.options.allSatisfy { !$0.text.isEmpty })
|
||||
XCTAssertFalse(context.question.isEmpty)
|
||||
XCTAssertTrue(context.viewState.isSubmitButtonDisabled)
|
||||
XCTAssertFalse(context.viewState.bindings.isUndisclosed)
|
||||
#expect(context.options.count == 3)
|
||||
#expect(context.options.allSatisfy { !$0.text.isEmpty })
|
||||
#expect(!context.question.isEmpty)
|
||||
#expect(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.bindings.isUndisclosed)
|
||||
|
||||
// Cancellation should work without confirmation
|
||||
let deferred = deferFulfillment(viewModel.actions) { _ in true }
|
||||
context.send(viewAction: .cancel)
|
||||
let action = try await deferred.fulfill()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
XCTAssertEqual(action, .close)
|
||||
#expect(context.alertInfo == nil)
|
||||
#expect(action == .close)
|
||||
}
|
||||
|
||||
func testNewPollInvalidEmptyOption() {
|
||||
@Test
|
||||
mutating func newPollInvalidEmptyOption() {
|
||||
setupViewModel()
|
||||
|
||||
context.question = "foo"
|
||||
context.options[0].text = "bla"
|
||||
context.options[1].text = "bla"
|
||||
context.send(viewAction: .addOption)
|
||||
XCTAssertTrue(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(context.viewState.isSubmitButtonDisabled)
|
||||
}
|
||||
|
||||
func testEditPollInvalidEmptyOption() {
|
||||
@Test
|
||||
mutating func editPollInvalidEmptyOption() {
|
||||
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
|
||||
|
||||
context.send(viewAction: .addOption)
|
||||
XCTAssertTrue(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(context.viewState.isSubmitButtonDisabled)
|
||||
|
||||
// Cancellation requires a confirmation
|
||||
context.send(viewAction: .cancel)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testEditPollSubmitButtonState() {
|
||||
@Test
|
||||
mutating func editPollSubmitButtonState() {
|
||||
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
|
||||
|
||||
XCTAssertTrue(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(context.viewState.isSubmitButtonDisabled)
|
||||
context.options[0].text = "foo"
|
||||
XCTAssertFalse(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.isSubmitButtonDisabled)
|
||||
|
||||
// Cancellation requires a confirmation
|
||||
context.send(viewAction: .cancel)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
}
|
||||
|
||||
func testNewPollSubmit() async throws {
|
||||
@Test
|
||||
mutating func newPollSubmit() async throws {
|
||||
setupViewModel()
|
||||
|
||||
context.question = "foo"
|
||||
context.options[0].text = "bla1"
|
||||
context.options[1].text = "bla2"
|
||||
XCTAssertFalse(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.isSubmitButtonDisabled)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
|
||||
let expectation = XCTestExpectation(description: "Create poll")
|
||||
timelineProxy.createPollQuestionAnswersPollKindClosure = { question, options, kind in
|
||||
XCTAssertEqual(question, "foo")
|
||||
XCTAssertEqual(options.count, 2)
|
||||
XCTAssertEqual(options[0], "bla1")
|
||||
XCTAssertEqual(options[1], "bla2")
|
||||
XCTAssertEqual(kind, .disclosed)
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .submit)
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
try await deferred.fulfill()
|
||||
try await confirmation { confirmation in
|
||||
timelineProxy.createPollQuestionAnswersPollKindClosure = { question, options, kind in
|
||||
#expect(question == "foo")
|
||||
#expect(options.count == 2)
|
||||
#expect(options[0] == "bla1")
|
||||
#expect(options[1] == "bla2")
|
||||
#expect(kind == .disclosed)
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .submit)
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testEditPollSubmit() async throws {
|
||||
@Test
|
||||
mutating func editPollSubmit() async throws {
|
||||
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
|
||||
|
||||
context.question = "What is your favorite country?"
|
||||
context.options.append(.init(text: "France 🇫🇷"))
|
||||
XCTAssertFalse(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.isSubmitButtonDisabled)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
|
||||
let expectation = XCTestExpectation(description: "Edit poll")
|
||||
timelineProxy.editPollOriginalQuestionAnswersPollKindClosure = { eventID, question, options, kind in
|
||||
XCTAssertEqual(eventID, "foo")
|
||||
XCTAssertEqual(question, "What is your favorite country?")
|
||||
XCTAssertEqual(options.count, 4)
|
||||
XCTAssertEqual(options[0], "Italy 🇮🇹")
|
||||
XCTAssertEqual(options[1], "China 🇨🇳")
|
||||
XCTAssertEqual(options[2], "USA 🇺🇸")
|
||||
XCTAssertEqual(options[3], "France 🇫🇷")
|
||||
XCTAssertEqual(kind, .disclosed)
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .submit)
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
try await deferred.fulfill()
|
||||
try await confirmation { confirmation in
|
||||
timelineProxy.editPollOriginalQuestionAnswersPollKindClosure = { eventID, question, options, kind in
|
||||
#expect(eventID == "foo")
|
||||
#expect(question == "What is your favorite country?")
|
||||
#expect(options.count == 4)
|
||||
#expect(options[0] == "Italy 🇮🇹")
|
||||
#expect(options[1] == "China 🇨🇳")
|
||||
#expect(options[2] == "USA 🇺🇸")
|
||||
#expect(options[3] == "France 🇫🇷")
|
||||
#expect(kind == .disclosed)
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
context.send(viewAction: .submit)
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func testDeletePoll() async throws {
|
||||
@Test
|
||||
mutating func deletePoll() async throws {
|
||||
setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed))
|
||||
|
||||
context.question = "What is your favorite country?"
|
||||
context.options.append(.init(text: "France 🇫🇷"))
|
||||
XCTAssertFalse(context.viewState.isSubmitButtonDisabled)
|
||||
#expect(!context.viewState.isSubmitButtonDisabled)
|
||||
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: 1, message: "The alert should be shown.") { $0 == .close }
|
||||
let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1)) { $0 == .close }
|
||||
context.send(viewAction: .delete)
|
||||
|
||||
try await deferredFailure.fulfill()
|
||||
XCTAssertNotNil(context.alertInfo, "An alert should be shown before deleting the poll.")
|
||||
#expect(context.alertInfo != nil, "An alert should be shown before deleting the poll.")
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .close }
|
||||
let expectation = XCTestExpectation(description: "Delete poll")
|
||||
timelineProxy.redactReasonClosure = { eventID, _ in
|
||||
XCTAssertEqual(eventID, .eventID("foo"))
|
||||
expectation.fulfill()
|
||||
return .success(())
|
||||
}
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
try await deferred.fulfill()
|
||||
try await confirmation { confirmation in
|
||||
var redactReasonCalled = false
|
||||
timelineProxy.redactReasonClosure = { eventID, _ in
|
||||
defer {
|
||||
confirmation()
|
||||
redactReasonCalled = true
|
||||
}
|
||||
#expect(eventID == .eventID("foo"))
|
||||
return .success(())
|
||||
}
|
||||
context.alertInfo?.secondaryButton?.action?()
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Since the redactReasonClosure is called asynchronously after closing the alert
|
||||
// We need to actively wait for the redactReasonClosure to be called before fulfilling the test.
|
||||
while !redactReasonCalled {
|
||||
await Task.yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(mode: PollFormMode = .new) {
|
||||
private mutating func setupViewModel(mode: PollFormMode = .new) {
|
||||
viewModel = PollFormScreenViewModel(mode: mode,
|
||||
timelineController: MockTimelineController(timelineProxy: timelineProxy),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
|
||||
@@ -9,89 +9,97 @@
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
final class QRCodeLoginScreenViewModelTests: XCTestCase {
|
||||
private var qrLoginProgressSubject: CurrentValueSubject<QRLoginProgress, AuthenticationServiceError>!
|
||||
private var qrCodeLoginService: QRCodeLoginServiceMock!
|
||||
@Suite
|
||||
struct QRCodeLoginScreenViewModelTests {
|
||||
private enum Mode { case login, linkDesktop, linkMobile }
|
||||
|
||||
private var linkMobileProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>!
|
||||
private var linkDesktopProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkDesktopProgress, QRCodeLoginError>!
|
||||
private var linkNewDeviceService: LinkNewDeviceServiceMock!
|
||||
var qrLoginProgressSubject: CurrentValueSubject<QRLoginProgress, AuthenticationServiceError>!
|
||||
var qrCodeLoginService: QRCodeLoginServiceMock!
|
||||
|
||||
private var appMediator: AppMediatorMock!
|
||||
var linkMobileProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkMobileProgress, QRCodeLoginError>!
|
||||
var linkDesktopProgressSubject: CurrentValueSubject<LinkNewDeviceService.LinkDesktopProgress, QRCodeLoginError>!
|
||||
var linkNewDeviceService: LinkNewDeviceServiceMock!
|
||||
|
||||
private var viewModel: QRCodeLoginScreenViewModelProtocol!
|
||||
private var context: QRCodeLoginScreenViewModelType.Context {
|
||||
var appMediator: AppMediatorMock!
|
||||
|
||||
var viewModel: QRCodeLoginScreenViewModelProtocol!
|
||||
var context: QRCodeLoginScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testLoginInitialState() {
|
||||
setupViewModel(mode: .login)
|
||||
@Test
|
||||
mutating func loginInitialState() {
|
||||
setup(mode: .login)
|
||||
|
||||
XCTAssertEqual(context.viewState.state, .loginInstructions)
|
||||
XCTAssertNil(context.qrResult)
|
||||
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
XCTAssertFalse(appMediator.requestAuthorizationIfNeededCalled)
|
||||
XCTAssertFalse(appMediator.openAppSettingsCalled)
|
||||
#expect(context.viewState.state == .loginInstructions)
|
||||
#expect(context.qrResult == nil)
|
||||
#expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
#expect(!appMediator.requestAuthorizationIfNeededCalled)
|
||||
#expect(!appMediator.openAppSettingsCalled)
|
||||
|
||||
XCTAssertFalse(linkNewDeviceService.linkMobileDeviceCalled)
|
||||
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
#expect(!linkNewDeviceService.linkMobileDeviceCalled)
|
||||
#expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
}
|
||||
|
||||
func testLinkDesktopInitialState() {
|
||||
setupViewModel(mode: .linkDesktop)
|
||||
@Test
|
||||
mutating func linkDesktopInitialState() {
|
||||
setup(mode: .linkDesktop)
|
||||
|
||||
XCTAssertEqual(context.viewState.state, .linkDesktopInstructions)
|
||||
XCTAssertNil(context.qrResult)
|
||||
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
XCTAssertFalse(appMediator.requestAuthorizationIfNeededCalled)
|
||||
XCTAssertFalse(appMediator.openAppSettingsCalled)
|
||||
#expect(context.viewState.state == .linkDesktopInstructions)
|
||||
#expect(context.qrResult == nil)
|
||||
#expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
#expect(!appMediator.requestAuthorizationIfNeededCalled)
|
||||
#expect(!appMediator.openAppSettingsCalled)
|
||||
|
||||
XCTAssertFalse(linkNewDeviceService.linkMobileDeviceCalled)
|
||||
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
#expect(!linkNewDeviceService.linkMobileDeviceCalled)
|
||||
#expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
}
|
||||
|
||||
func testLinkMobileInitialState() {
|
||||
setupViewModel(mode: .linkMobile)
|
||||
@Test
|
||||
mutating func linkMobileInitialState() {
|
||||
setup(mode: .linkMobile)
|
||||
|
||||
XCTAssertTrue(context.viewState.state.isDisplayQR)
|
||||
XCTAssertTrue(linkNewDeviceService.linkMobileDeviceCalled)
|
||||
#expect(context.viewState.state.isDisplayQR)
|
||||
#expect(linkNewDeviceService.linkMobileDeviceCalled)
|
||||
|
||||
XCTAssertFalse(linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
XCTAssertFalse(qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
XCTAssertNil(context.qrResult)
|
||||
#expect(!linkNewDeviceService.linkDesktopDeviceWithCalled)
|
||||
#expect(!qrCodeLoginService.loginWithQRCodeDataCalled)
|
||||
#expect(context.qrResult == nil)
|
||||
}
|
||||
|
||||
func testRequestCameraPermission() async throws {
|
||||
setupViewModel(mode: .login)
|
||||
@Test
|
||||
mutating func requestCameraPermission() async throws {
|
||||
setup(mode: .login)
|
||||
appMediator.requestAuthorizationIfNeededReturnValue = false
|
||||
XCTAssert(context.viewState.state == .loginInstructions)
|
||||
#expect(context.viewState.state == .loginInstructions)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||
state.state == .error(.noCameraPermission)
|
||||
}
|
||||
context.send(viewAction: .startScan)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled)
|
||||
#expect(appMediator.requestAuthorizationIfNeededCalled)
|
||||
|
||||
context.send(viewAction: .errorAction(.openSettings))
|
||||
await Task.yield()
|
||||
XCTAssertTrue(appMediator.openAppSettingsCalled)
|
||||
XCTAssertNil(context.qrResult)
|
||||
#expect(appMediator.openAppSettingsCalled)
|
||||
#expect(context.qrResult == nil)
|
||||
}
|
||||
|
||||
func testLogin() async throws {
|
||||
setupViewModel(mode: .login)
|
||||
XCTAssert(context.viewState.state == .loginInstructions)
|
||||
@Test
|
||||
mutating func login() async throws {
|
||||
setup(mode: .login)
|
||||
#expect(context.viewState.state == .loginInstructions)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.state == .scan(.scanning)
|
||||
}
|
||||
context.send(viewAction: .startScan)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled)
|
||||
#expect(appMediator.requestAuthorizationIfNeededCalled)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.state == .scan(.connecting)
|
||||
@@ -121,14 +129,15 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testLinkDesktopComputer() async throws {
|
||||
setupViewModel(mode: .linkDesktop)
|
||||
XCTAssert(context.viewState.state == .linkDesktopInstructions)
|
||||
@Test
|
||||
mutating func linkDesktopComputer() async throws {
|
||||
setup(mode: .linkDesktop)
|
||||
#expect(context.viewState.state == .linkDesktopInstructions)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.scanning) }
|
||||
context.send(viewAction: .startScan)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(appMediator.requestAuthorizationIfNeededCalled)
|
||||
#expect(appMediator.requestAuthorizationIfNeededCalled)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.state == .scan(.connecting) }
|
||||
context.qrResult = .init()
|
||||
@@ -146,7 +155,7 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
let currentState = context.viewState.state
|
||||
let deferredFailure = deferFailure(context.$viewState, timeout: 1) { $0.state != currentState }
|
||||
let deferredFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.state != currentState }
|
||||
linkDesktopProgressSubject.send(.syncingSecrets)
|
||||
try await deferredFailure.fulfill()
|
||||
|
||||
@@ -158,9 +167,10 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testLinkMobileDevice() async throws {
|
||||
setupViewModel(mode: .linkMobile)
|
||||
XCTAssert(context.viewState.state.isDisplayQR)
|
||||
@Test
|
||||
mutating func linkMobileDevice() async throws {
|
||||
setup(mode: .linkMobile)
|
||||
#expect(context.viewState.state.isDisplayQR)
|
||||
|
||||
let checkCodeSender = CheckCodeSenderSDKMock()
|
||||
let checkCodeSenderProxy = CheckCodeSenderProxy(underlyingSender: checkCodeSender)
|
||||
@@ -189,16 +199,14 @@ final class QRCodeLoginScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
let currentState = context.viewState.state
|
||||
let deferredFailure = deferFailure(context.$viewState, timeout: 1) { $0.state != currentState }
|
||||
let deferredFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.state != currentState }
|
||||
linkMobileProgressSubject.send(.done)
|
||||
try await deferredFailure.fulfill()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
enum Mode { case login, linkDesktop, linkMobile }
|
||||
|
||||
private func setupViewModel(mode: Mode) {
|
||||
private mutating func setup(mode: Mode) {
|
||||
qrLoginProgressSubject = .init(.starting)
|
||||
qrCodeLoginService = QRCodeLoginServiceMock()
|
||||
qrCodeLoginService.loginWithQRCodeDataReturnValue = qrLoginProgressSubject.asCurrentValuePublisher()
|
||||
|
||||
@@ -7,42 +7,45 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RemotePreferenceTests: XCTestCase {
|
||||
func testOverrideAndReset() {
|
||||
@Suite
|
||||
struct RemotePreferenceTests {
|
||||
@Test
|
||||
func overrideAndReset() {
|
||||
let preference = RemotePreference(0)
|
||||
XCTAssertEqual(preference.publisher.value, 0)
|
||||
XCTAssertFalse(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == 0)
|
||||
#expect(!preference.isRemotelyConfigured)
|
||||
|
||||
preference.applyRemoteValue(1)
|
||||
XCTAssertEqual(preference.publisher.value, 1)
|
||||
XCTAssertTrue(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == 1)
|
||||
#expect(preference.isRemotelyConfigured)
|
||||
|
||||
preference.applyRemoteValue(2)
|
||||
XCTAssertEqual(preference.publisher.value, 2)
|
||||
XCTAssertTrue(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == 2)
|
||||
#expect(preference.isRemotelyConfigured)
|
||||
|
||||
preference.reset()
|
||||
XCTAssertEqual(preference.publisher.value, 0)
|
||||
XCTAssertFalse(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == 0)
|
||||
#expect(!preference.isRemotelyConfigured)
|
||||
}
|
||||
|
||||
func testOptionalOverride() {
|
||||
@Test
|
||||
func optionalOverride() {
|
||||
let preference: RemotePreference<String?> = .init("Hello")
|
||||
XCTAssertEqual(preference.publisher.value, "Hello")
|
||||
XCTAssertFalse(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == "Hello")
|
||||
#expect(!preference.isRemotelyConfigured)
|
||||
|
||||
preference.applyRemoteValue("World")
|
||||
XCTAssertEqual(preference.publisher.value, "World")
|
||||
XCTAssertTrue(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == "World")
|
||||
#expect(preference.isRemotelyConfigured)
|
||||
|
||||
preference.applyRemoteValue(nil)
|
||||
XCTAssertEqual(preference.publisher.value, nil)
|
||||
XCTAssertTrue(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == nil)
|
||||
#expect(preference.isRemotelyConfigured)
|
||||
|
||||
preference.reset()
|
||||
XCTAssertEqual(preference.publisher.value, "Hello")
|
||||
XCTAssertFalse(preference.isRemotelyConfigured)
|
||||
#expect(preference.publisher.value == "Hello")
|
||||
#expect(!preference.isRemotelyConfigured)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ReportContentScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct ReportContentScreenViewModelTests {
|
||||
let eventID = "test-id"
|
||||
let senderID = "@meany:server.com"
|
||||
let reportReason = "I don't like it."
|
||||
|
||||
func testReportContent() async throws {
|
||||
@Test
|
||||
func reportContent() async throws {
|
||||
// Given the report content view for some content.
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "test"))
|
||||
roomProxy.reportContentReasonReturnValue = .success(())
|
||||
@@ -37,14 +39,15 @@ class ReportContentScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the content should be reported, but the user should not be included.
|
||||
XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.")
|
||||
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.")
|
||||
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.")
|
||||
XCTAssertEqual(clientProxy.ignoreUserCallsCount, 0, "A call to ignore a user should not have been made.")
|
||||
XCTAssertNil(clientProxy.ignoreUserReceivedUserID, "The sender shouldn't have been ignored.")
|
||||
#expect(roomProxy.reportContentReasonCallsCount == 1, "The content should always be reported.")
|
||||
#expect(roomProxy.reportContentReasonReceivedArguments?.eventID == eventID, "The event ID should match the content being reported.")
|
||||
#expect(roomProxy.reportContentReasonReceivedArguments?.reason == reportReason, "The reason should match the user input.")
|
||||
#expect(clientProxy.ignoreUserCallsCount == 0, "A call to ignore a user should not have been made.")
|
||||
#expect(clientProxy.ignoreUserReceivedUserID == nil, "The sender shouldn't have been ignored.")
|
||||
}
|
||||
|
||||
func testReportIgnoringSender() async throws {
|
||||
@Test
|
||||
func reportIgnoringSender() async throws {
|
||||
// Given the report content view for some content.
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "test"))
|
||||
roomProxy.reportContentReasonReturnValue = .success(())
|
||||
@@ -67,10 +70,10 @@ class ReportContentScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the content should be reported, and the user should be ignored.
|
||||
XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.")
|
||||
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, eventID, "The event ID should match the content being reported.")
|
||||
XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.")
|
||||
XCTAssertEqual(clientProxy.ignoreUserCallsCount, 1, "A call should have been made to ignore the sender.")
|
||||
XCTAssertEqual(clientProxy.ignoreUserReceivedUserID, senderID, "The ignored user ID should match the sender.")
|
||||
#expect(roomProxy.reportContentReasonCallsCount == 1, "The content should always be reported.")
|
||||
#expect(roomProxy.reportContentReasonReceivedArguments?.eventID == eventID, "The event ID should match the content being reported.")
|
||||
#expect(roomProxy.reportContentReasonReceivedArguments?.reason == reportReason, "The reason should match the user input.")
|
||||
#expect(clientProxy.ignoreUserCallsCount == 1, "A call should have been made to ignore the sender.")
|
||||
#expect(clientProxy.ignoreUserReceivedUserID == senderID, "The ignored user ID should match the sender.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,96 +7,108 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ReportRoomScreenViewModelTests: XCTestCase {
|
||||
var viewModel: ReportRoomScreenViewModelProtocol!
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
@Suite
|
||||
struct ReportRoomScreenViewModelTests {
|
||||
private var viewModel: ReportRoomScreenViewModelProtocol
|
||||
private var roomProxy: JoinedRoomProxyMock
|
||||
|
||||
var context: ReportRoomScreenViewModelType.Context {
|
||||
private var context: ReportRoomScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
roomProxy = JoinedRoomProxyMock(.init())
|
||||
viewModel = ReportRoomScreenViewModel(roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertTrue(context.viewState.bindings.reason.isEmpty)
|
||||
XCTAssertFalse(context.viewState.bindings.shouldLeaveRoom)
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(context.viewState.bindings.reason.isEmpty)
|
||||
#expect(!context.viewState.bindings.shouldLeaveRoom)
|
||||
}
|
||||
|
||||
func testReportSuccess() async throws {
|
||||
@Test
|
||||
func reportSuccess() async throws {
|
||||
let reason = "Spam"
|
||||
let expectation = XCTestExpectation(description: "Report success")
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
defer { expectation.fulfill() }
|
||||
XCTAssertEqual(reasonArgument, reason)
|
||||
return .success(())
|
||||
|
||||
try await confirmation { confirmation in
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
#expect(reasonArgument == reason)
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss(shouldLeaveRoom: false)
|
||||
}
|
||||
|
||||
context.reason = reason
|
||||
context.send(viewAction: .report)
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss(shouldLeaveRoom: false)
|
||||
}
|
||||
|
||||
context.reason = reason
|
||||
context.send(viewAction: .report)
|
||||
|
||||
try await deferred.fulfill()
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testReportAndLeaveSuccess() async throws {
|
||||
@Test
|
||||
func reportAndLeaveSuccess() async throws {
|
||||
let reason = "Spam"
|
||||
let reportExpectation = XCTestExpectation(description: "Report success")
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
defer { reportExpectation.fulfill() }
|
||||
XCTAssertEqual(reasonArgument, reason)
|
||||
return .success(())
|
||||
|
||||
try await confirmation(expectedCount: 2) { confirmation in
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
#expect(reasonArgument == reason)
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
roomProxy.leaveRoomClosure = {
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss(shouldLeaveRoom: true)
|
||||
}
|
||||
|
||||
context.reason = reason
|
||||
context.shouldLeaveRoom = true
|
||||
context.send(viewAction: .report)
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
let leaveExpectation = XCTestExpectation(description: "Leave success")
|
||||
roomProxy.leaveRoomClosure = {
|
||||
defer { leaveExpectation.fulfill() }
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { action in
|
||||
action == .dismiss(shouldLeaveRoom: true)
|
||||
}
|
||||
|
||||
context.reason = reason
|
||||
context.shouldLeaveRoom = true
|
||||
context.send(viewAction: .report)
|
||||
|
||||
await fulfillment(of: [reportExpectation, leaveExpectation])
|
||||
try await deferred.fulfill()
|
||||
#expect(roomProxy.reportRoomReasonCalled)
|
||||
#expect(roomProxy.leaveRoomCalled)
|
||||
}
|
||||
|
||||
func testReportSuccessLeaveFails() async throws {
|
||||
@Test
|
||||
func reportSuccessLeaveFails() async throws {
|
||||
let reason = "Spam"
|
||||
let reportExpectation = XCTestExpectation(description: "Report success")
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
defer { reportExpectation.fulfill() }
|
||||
XCTAssertEqual(reasonArgument, reason)
|
||||
return .success(())
|
||||
|
||||
try await confirmation(expectedCount: 2) { confirmation in
|
||||
roomProxy.reportRoomReasonClosure = { reasonArgument in
|
||||
#expect(reasonArgument == reason)
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
roomProxy.leaveRoomClosure = {
|
||||
confirmation()
|
||||
return .failure(.eventNotFound)
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alert)) { $0 != nil }
|
||||
|
||||
context.reason = reason
|
||||
context.shouldLeaveRoom = true
|
||||
context.send(viewAction: .report)
|
||||
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
let leaveExpectation = XCTestExpectation(description: "Leave fails")
|
||||
roomProxy.leaveRoomClosure = {
|
||||
defer { leaveExpectation.fulfill() }
|
||||
return .failure(.eventNotFound)
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.bindings.alert)) { $0 != nil }
|
||||
|
||||
context.reason = reason
|
||||
context.shouldLeaveRoom = true
|
||||
context.send(viewAction: .report)
|
||||
|
||||
await fulfillment(of: [reportExpectation, leaveExpectation])
|
||||
try await deferred.fulfill()
|
||||
#expect(roomProxy.reportRoomReasonCalled)
|
||||
#expect(roomProxy.leaveRoomCalled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,47 +7,48 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
|
||||
let roomProxy = JoinedRoomProxyMock(.init())
|
||||
var viewModel: ResolveVerifiedUserSendFailureScreenViewModel!
|
||||
var context: ResolveVerifiedUserSendFailureScreenViewModel.Context {
|
||||
viewModel.context
|
||||
}
|
||||
@Suite
|
||||
struct ResolveVerifiedUserSendFailureScreenViewModelTests {
|
||||
private let roomProxy = JoinedRoomProxyMock(.init())
|
||||
|
||||
func testUnsignedDevice() async throws {
|
||||
@Test
|
||||
func unsignedDevice() async throws {
|
||||
// Given a failure where a single user has an unverified device
|
||||
let userID = "@alice:matrix.org"
|
||||
viewModel = makeViewModel(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]]))
|
||||
let viewModel = makeViewModel(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]]))
|
||||
|
||||
try await verifyResolving(userIDs: [userID])
|
||||
try await verifyResolving(viewModel: viewModel, userIDs: [userID])
|
||||
}
|
||||
|
||||
func testMultipleUnsignedDevices() async throws {
|
||||
@Test
|
||||
func multipleUnsignedDevices() async throws {
|
||||
// Given a failure where a multiple users have unverified devices.
|
||||
let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"]
|
||||
let devices = Dictionary(uniqueKeysWithValues: userIDs.map { ($0, ["DEVICE1, DEVICE2"]) })
|
||||
viewModel = makeViewModel(with: .hasUnsignedDevice(devices: devices))
|
||||
let viewModel = makeViewModel(with: .hasUnsignedDevice(devices: devices))
|
||||
|
||||
try await verifyResolving(userIDs: userIDs, assertStrings: false)
|
||||
try await verifyResolving(viewModel: viewModel, userIDs: userIDs, assertStrings: false)
|
||||
}
|
||||
|
||||
func testChangedIdentity() async throws {
|
||||
@Test
|
||||
func changedIdentity() async throws {
|
||||
// Given a failure where a single user's identity has changed.
|
||||
let userID = "@alice:matrix.org"
|
||||
viewModel = makeViewModel(with: .changedIdentity(users: [userID]))
|
||||
let viewModel = makeViewModel(with: .changedIdentity(users: [userID]))
|
||||
|
||||
try await verifyResolving(userIDs: [userID])
|
||||
try await verifyResolving(viewModel: viewModel, userIDs: [userID])
|
||||
}
|
||||
|
||||
func testMultipleChangedIdentities() async throws {
|
||||
@Test
|
||||
func multipleChangedIdentities() async throws {
|
||||
// Given a failure where a multiple users have unverified devices.
|
||||
let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"]
|
||||
viewModel = makeViewModel(with: .changedIdentity(users: userIDs))
|
||||
let viewModel = makeViewModel(with: .changedIdentity(users: userIDs))
|
||||
|
||||
try await verifyResolving(userIDs: userIDs)
|
||||
try await verifyResolving(viewModel: viewModel, userIDs: userIDs)
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
@@ -59,17 +60,18 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
private func verifyResolving(userIDs: [String], assertStrings: Bool = true) async throws {
|
||||
private func verifyResolving(viewModel: ResolveVerifiedUserSendFailureScreenViewModel, userIDs: [String], assertStrings: Bool = true) async throws {
|
||||
var remainingUserIDs = userIDs
|
||||
let context = viewModel.context
|
||||
|
||||
while remainingUserIDs.count > 1 {
|
||||
// Verify that the strings are being updated.
|
||||
if assertStrings {
|
||||
verifyDisplayName(from: remainingUserIDs)
|
||||
try verifyDisplayName(context: context, from: remainingUserIDs)
|
||||
}
|
||||
|
||||
// When resolving the first failure.
|
||||
let deferredFailure = deferFailure(viewModel.actionsPublisher, timeout: 1) { $0 == .dismiss }
|
||||
let deferredFailure = deferFailure(viewModel.actionsPublisher, timeout: .seconds(1)) { $0 == .dismiss }
|
||||
context.send(viewAction: .resolveAndResend)
|
||||
|
||||
// Then the sheet should remain open for the next failure.
|
||||
@@ -80,7 +82,7 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
|
||||
|
||||
// Verify the final string.
|
||||
if assertStrings {
|
||||
verifyDisplayName(from: remainingUserIDs)
|
||||
try verifyDisplayName(context: context, from: remainingUserIDs)
|
||||
}
|
||||
|
||||
// When resolving the final failure.
|
||||
@@ -91,18 +93,12 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
private func verifyDisplayName(from remainingUserIDs: [String]) {
|
||||
guard let userID = remainingUserIDs.first else {
|
||||
XCTFail("There should be a user ID to check.")
|
||||
return
|
||||
}
|
||||
private func verifyDisplayName(context: ResolveVerifiedUserSendFailureScreenViewModel.Context, from remainingUserIDs: [String]) throws {
|
||||
let userID = try #require(remainingUserIDs.first, "There should be a user ID to check.")
|
||||
let displayName = try #require(roomProxy.membersPublisher.value.first { $0.userID == userID }?.displayName,
|
||||
"There should be a matching mock user")
|
||||
|
||||
guard let displayName = roomProxy.membersPublisher.value.first(where: { $0.userID == userID })?.displayName else {
|
||||
XCTFail("There should be a matching mock user")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(context.viewState.title.contains(displayName))
|
||||
XCTAssertTrue(context.viewState.subtitle.contains(displayName))
|
||||
#expect(context.viewState.title.contains(displayName))
|
||||
#expect(context.viewState.subtitle.contains(displayName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RestorationTokenTests: XCTestCase {
|
||||
func testDecodeTokenWithSlidingSyncProxy() throws {
|
||||
@Suite
|
||||
struct RestorationTokenTests {
|
||||
@Test
|
||||
func decodeTokenWithSlidingSyncProxy() throws {
|
||||
// Given an encoded restoration token that contains a session with a sliding sync proxy.
|
||||
let originalToken = RestorationTokenV4(session: SessionV1(accessToken: "1234",
|
||||
refreshToken: "5678",
|
||||
@@ -26,18 +29,14 @@ class RestorationTokenTests: XCTestCase {
|
||||
let data = try JSONEncoder().encode(originalToken)
|
||||
|
||||
// When decoding the data to the current restoration token format.
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(RestorationToken.self, from: data)) { error in
|
||||
// Then an error should be thrown as it is no longer supported.
|
||||
switch error {
|
||||
case RestorationTokenError.slidingSyncProxyNotSupported:
|
||||
break
|
||||
default:
|
||||
XCTFail("Unexpected error thrown: \(error)")
|
||||
}
|
||||
// Then an error should be thrown as it is no longer supported.
|
||||
#expect(throws: RestorationTokenError.slidingSyncProxyNotSupported) {
|
||||
try JSONDecoder().decode(RestorationToken.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
func testDecodeFromTokenV4() throws {
|
||||
@Test
|
||||
func decodeFromTokenV4() throws {
|
||||
// Given an encoded restoration token in the 4th format that contains a stored session directory.
|
||||
let sessionDirectoryName = UUID().uuidString
|
||||
let originalToken = RestorationTokenV4(session: SessionV1(accessToken: "1234",
|
||||
@@ -57,16 +56,17 @@ class RestorationTokenTests: XCTestCase {
|
||||
|
||||
// Then the output should be a valid token with the expected store directories.
|
||||
assertEqual(session: decodedToken.session, originalSession: originalToken.session)
|
||||
XCTAssertEqual(decodedToken.passphrase, originalToken.passphrase, "The passphrase should not be changed.")
|
||||
XCTAssertEqual(decodedToken.pusherNotificationClientIdentifier, originalToken.pusherNotificationClientIdentifier,
|
||||
"The push notification client identifier should not be changed.")
|
||||
XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory,
|
||||
"The session directory should not be changed.")
|
||||
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName),
|
||||
"The cache directory should be derived from the session directory but in the caches directory.")
|
||||
#expect(decodedToken.passphrase == originalToken.passphrase, "The passphrase should not be changed.")
|
||||
#expect(decodedToken.pusherNotificationClientIdentifier == originalToken.pusherNotificationClientIdentifier,
|
||||
"The push notification client identifier should not be changed.")
|
||||
#expect(decodedToken.sessionDirectories.dataDirectory == originalToken.sessionDirectory,
|
||||
"The session directory should not be changed.")
|
||||
#expect(decodedToken.sessionDirectories.cacheDirectory == .sessionCachesBaseDirectory.appending(component: sessionDirectoryName),
|
||||
"The cache directory should be derived from the session directory but in the caches directory.")
|
||||
}
|
||||
|
||||
func testDecodeFromTokenV5() throws {
|
||||
@Test
|
||||
func decodeFromTokenV5() throws {
|
||||
// Given an encoded restoration token in the 5th format that contains separate directories for session data and caches.
|
||||
let sessionDirectoryName = UUID().uuidString
|
||||
let originalToken = RestorationTokenV5(session: SessionV1(accessToken: "1234",
|
||||
@@ -87,16 +87,17 @@ class RestorationTokenTests: XCTestCase {
|
||||
|
||||
// Then the output should be a valid token.
|
||||
assertEqual(session: decodedToken.session, originalSession: originalToken.session)
|
||||
XCTAssertEqual(decodedToken.passphrase, originalToken.passphrase, "The passphrase should not be changed.")
|
||||
XCTAssertEqual(decodedToken.pusherNotificationClientIdentifier, originalToken.pusherNotificationClientIdentifier,
|
||||
"The push notification client identifier should not be changed.")
|
||||
XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory,
|
||||
"The session directory should not be changed.")
|
||||
XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, originalToken.cacheDirectory,
|
||||
"The cache directory should not be changed.")
|
||||
#expect(decodedToken.passphrase == originalToken.passphrase, "The passphrase should not be changed.")
|
||||
#expect(decodedToken.pusherNotificationClientIdentifier == originalToken.pusherNotificationClientIdentifier,
|
||||
"The push notification client identifier should not be changed.")
|
||||
#expect(decodedToken.sessionDirectories.dataDirectory == originalToken.sessionDirectory,
|
||||
"The session directory should not be changed.")
|
||||
#expect(decodedToken.sessionDirectories.cacheDirectory == originalToken.cacheDirectory,
|
||||
"The cache directory should not be changed.")
|
||||
}
|
||||
|
||||
func testDecodeFromCurrentToken() throws {
|
||||
@Test
|
||||
func decodeFromCurrentToken() throws {
|
||||
// Given an encoded restoration token in the current format.
|
||||
let originalToken = RestorationToken(session: Session(accessToken: "1234",
|
||||
refreshToken: "5678",
|
||||
@@ -114,16 +115,16 @@ class RestorationTokenTests: XCTestCase {
|
||||
let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data)
|
||||
|
||||
// Then the output should be a valid token.
|
||||
XCTAssertEqual(decodedToken, originalToken, "The token should remain identical.")
|
||||
#expect(decodedToken == originalToken, "The token should remain identical.")
|
||||
}
|
||||
|
||||
func assertEqual(session: Session, originalSession: SessionV1) {
|
||||
XCTAssertEqual(session.accessToken, originalSession.accessToken, "The access token should not be changed.")
|
||||
XCTAssertEqual(session.refreshToken, originalSession.refreshToken, "The refresh token should not be changed.")
|
||||
XCTAssertEqual(session.userId, originalSession.userId, "The user ID should not be changed.")
|
||||
XCTAssertEqual(session.deviceId, originalSession.deviceId, "The device ID should not be changed.")
|
||||
XCTAssertEqual(session.homeserverUrl, originalSession.homeserverUrl, "The homeserver URL should not be changed.")
|
||||
XCTAssertEqual(session.oidcData, originalSession.oidcData, "The OIDC data should not be changed.")
|
||||
#expect(session.accessToken == originalSession.accessToken, "The access token should not be changed.")
|
||||
#expect(session.refreshToken == originalSession.refreshToken, "The refresh token should not be changed.")
|
||||
#expect(session.userId == originalSession.userId, "The user ID should not be changed.")
|
||||
#expect(session.deviceId == originalSession.deviceId, "The device ID should not be changed.")
|
||||
#expect(session.homeserverUrl == originalSession.homeserverUrl, "The homeserver URL should not be changed.")
|
||||
#expect(session.oidcData == originalSession.oidcData, "The OIDC data should not be changed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomChangePermissionsScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomChangePermissionsScreenViewModelTests {
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
var viewModel: RoomChangePermissionsScreenViewModelProtocol!
|
||||
|
||||
@@ -18,67 +19,62 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testChangeSetting() {
|
||||
setUp(isSpace: false)
|
||||
@Test
|
||||
mutating func changeSetting() throws {
|
||||
setup(isSpace: false)
|
||||
// Given a screen with no changes.
|
||||
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else {
|
||||
XCTFail("There should be a setting for the room avatar.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .moderator)
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
let index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
|
||||
"There should be a setting for the room avatar.")
|
||||
#expect(context.settings[.roomDetails]?[index].roleValue == .moderator)
|
||||
#expect(!context.viewState.hasChanges)
|
||||
|
||||
// When updating a setting.
|
||||
let setting = RoomPermissionsSetting(title: "",
|
||||
value: RoomRole.user.powerLevelValue,
|
||||
ownPowerLevel: RoomRole.creator.powerLevel,
|
||||
keyPath: \.roomAvatar)
|
||||
XCTAssertFalse(setting.isDisabled)
|
||||
XCTAssertEqual(setting.availableValues.map(\.tag), RoomPermissionsSetting.allValues.map(\.tag))
|
||||
#expect(!setting.isDisabled)
|
||||
#expect(setting.availableValues.map(\.tag) == RoomPermissionsSetting.allValues.map(\.tag))
|
||||
context.settings[.roomDetails]?[index] = setting
|
||||
|
||||
// Then the setting should update and the changes should be flagged.
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .user)
|
||||
XCTAssertTrue(context.viewState.hasChanges)
|
||||
#expect(context.settings[.roomDetails]?[index].roleValue == .user)
|
||||
#expect(context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testSettingsCantBeChanged() {
|
||||
setUp(isSpace: false, ownPowerLevel: .value(25))
|
||||
@Test
|
||||
mutating func settingsCantBeChanged() throws {
|
||||
setup(isSpace: false, ownPowerLevel: .value(25))
|
||||
// Given a screen with no changes.
|
||||
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else {
|
||||
XCTFail("There should be a setting for the room avatar.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .moderator)
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].isDisabled, true)
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].availableValues.count, 1)
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
var index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
|
||||
"There should be a setting for the room avatar.")
|
||||
#expect(context.settings[.roomDetails]?[index].roleValue == .moderator)
|
||||
#expect(context.settings[.roomDetails]?[index].isDisabled == true)
|
||||
#expect(context.settings[.roomDetails]?[index].availableValues.count == 1)
|
||||
#expect(!context.viewState.hasChanges)
|
||||
|
||||
guard let index = context.settings[.messagesAndContent]?.firstIndex(where: { $0.keyPath == \.eventsDefault }) else {
|
||||
XCTFail("There should be a setting for the events.")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(context.settings[.messagesAndContent]?[index].roleValue, .user)
|
||||
XCTAssertEqual(context.settings[.messagesAndContent]?[index].isDisabled, false)
|
||||
XCTAssertEqual(context.settings[.messagesAndContent]?[index].availableValues.count, 1)
|
||||
index = try #require(context.settings[.messagesAndContent]?.firstIndex { $0.keyPath == \.eventsDefault },
|
||||
"There should be a setting for the events.")
|
||||
#expect(context.settings[.messagesAndContent]?[index].roleValue == .user)
|
||||
#expect(context.settings[.messagesAndContent]?[index].isDisabled == false)
|
||||
#expect(context.settings[.messagesAndContent]?[index].availableValues.count == 1)
|
||||
}
|
||||
|
||||
func testSave() async throws {
|
||||
setUp(isSpace: false)
|
||||
@Test
|
||||
mutating func save() async throws {
|
||||
setup(isSpace: false)
|
||||
// Given a screen with changes.
|
||||
guard let index = context.settings[.roomDetails]?.firstIndex(where: { $0.keyPath == \.roomAvatar }) else {
|
||||
XCTFail("There should be a setting for the room avatar.")
|
||||
return
|
||||
}
|
||||
let index = try #require(context.settings[.roomDetails]?.firstIndex { $0.keyPath == \.roomAvatar },
|
||||
"There should be a setting for the room avatar.")
|
||||
context.settings[.roomDetails]?[index] = RoomPermissionsSetting(title: "",
|
||||
value: RoomRole.user.powerLevelValue,
|
||||
ownPowerLevel: RoomRole.creator.powerLevel,
|
||||
keyPath: \.roomAvatar)
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].roleValue, .user)
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].isDisabled, false)
|
||||
XCTAssertEqual(context.settings[.roomDetails]?[index].availableValues.map(\.tag), RoomPermissionsSetting.allValues.map(\.tag))
|
||||
XCTAssertTrue(context.viewState.hasChanges)
|
||||
XCTAssertEqual(context.settings.count, 3)
|
||||
#expect(context.settings[.roomDetails]?[index].roleValue == .user)
|
||||
#expect(context.settings[.roomDetails]?[index].isDisabled == false)
|
||||
#expect(context.settings[.roomDetails]?[index].availableValues.map(\.tag) == RoomPermissionsSetting.allValues.map(\.tag))
|
||||
#expect(context.viewState.hasChanges)
|
||||
#expect(context.settings.count == 3)
|
||||
|
||||
// When saving changes.
|
||||
context.send(viewAction: .save)
|
||||
@@ -86,40 +82,45 @@ class RoomChangePermissionsScreenViewModelTests: XCTestCase {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then the changes should be applied.
|
||||
XCTAssertTrue(roomProxy.applyPowerLevelChangesCalled)
|
||||
XCTAssertEqual(roomProxy.applyPowerLevelChangesReceivedChanges, .init(roomAvatar: 0),
|
||||
"Only the avatar setting should be applied. No other settings were changed so they should be nil to remain left alone.")
|
||||
#expect(roomProxy.applyPowerLevelChangesCalled)
|
||||
#expect(roomProxy.applyPowerLevelChangesReceivedChanges == .init(roomAvatar: 0),
|
||||
"Only the avatar setting should be applied. No other settings were changed so they should be nil to remain left alone.")
|
||||
}
|
||||
|
||||
func testSaveNoChanges() {
|
||||
setUp(isSpace: false)
|
||||
@Test
|
||||
mutating func saveNoChanges() {
|
||||
setup(isSpace: false)
|
||||
// Given a screen with no changes.
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
#expect(!context.viewState.hasChanges)
|
||||
|
||||
// When saving changes.
|
||||
context.send(viewAction: .save)
|
||||
|
||||
// Then nothing should happen.
|
||||
XCTAssertFalse(roomProxy.applyPowerLevelChangesCalled)
|
||||
#expect(!roomProxy.applyPowerLevelChangesCalled)
|
||||
}
|
||||
|
||||
func testDefaultStateRoom() {
|
||||
setUp(isSpace: false)
|
||||
XCTAssertNotNil(context.settings[.roomDetails])
|
||||
XCTAssertNotNil(context.settings[.memberModeration])
|
||||
XCTAssertNotNil(context.settings[.messagesAndContent])
|
||||
XCTAssertNil(context.settings[.manageSpace])
|
||||
@Test
|
||||
mutating func defaultStateRoom() {
|
||||
setup(isSpace: false)
|
||||
#expect(context.settings[.roomDetails] != nil)
|
||||
#expect(context.settings[.memberModeration] != nil)
|
||||
#expect(context.settings[.messagesAndContent] != nil)
|
||||
#expect(context.settings[.manageSpace] == nil)
|
||||
}
|
||||
|
||||
func testDefaultStateSpace() {
|
||||
setUp(isSpace: true)
|
||||
XCTAssertNotNil(context.settings[.roomDetails])
|
||||
XCTAssertNotNil(context.settings[.memberModeration])
|
||||
XCTAssertNil(context.settings[.messagesAndContent])
|
||||
XCTAssertNotNil(context.settings[.manageSpace])
|
||||
@Test
|
||||
mutating func defaultStateSpace() {
|
||||
setup(isSpace: true)
|
||||
#expect(context.settings[.roomDetails] != nil)
|
||||
#expect(context.settings[.memberModeration] != nil)
|
||||
#expect(context.settings[.messagesAndContent] == nil)
|
||||
#expect(context.settings[.manageSpace] != nil)
|
||||
}
|
||||
|
||||
private func setUp(isSpace: Bool, ownPowerLevel: RoomPowerLevel = RoomRole.creator.powerLevel) {
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(isSpace: Bool, ownPowerLevel: RoomPowerLevel = RoomRole.creator.powerLevel) {
|
||||
roomProxy = JoinedRoomProxyMock(.init(isSpace: isSpace))
|
||||
viewModel = RoomChangePermissionsScreenViewModel(currentPermissions: .init(powerLevels: .mock),
|
||||
ownPowerLevel: ownPowerLevel,
|
||||
|
||||
@@ -7,149 +7,148 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomChangeRolesScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomChangeRolesScreenViewModelTests {
|
||||
var viewModel: RoomChangeRolesScreenViewModelProtocol!
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
|
||||
var context: RoomChangeRolesScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testInitialStateAdministrators() {
|
||||
setupViewModel(mode: .administrator)
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators)
|
||||
XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators)
|
||||
XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers)
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 2)
|
||||
XCTAssertEqual(context.viewState.membersWithRole.first?.id, RoomMemberProxyMock.mockAdmin.userID)
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
XCTAssertFalse(context.viewState.isSearching)
|
||||
}
|
||||
|
||||
func testInitialStateModerators() {
|
||||
setupViewModel(mode: .moderator)
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators)
|
||||
XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators)
|
||||
XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers)
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 3)
|
||||
XCTAssertNotNil(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID })
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
XCTAssertFalse(context.viewState.isSearching)
|
||||
|
||||
@Test
|
||||
mutating func initialStateAdministrators() {
|
||||
setup(mode: .administrator)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.administrators == context.viewState.visibleAdministrators)
|
||||
#expect(context.viewState.moderators == context.viewState.visibleModerators)
|
||||
#expect(context.viewState.users == context.viewState.visibleUsers)
|
||||
#expect(context.viewState.membersWithRole.count == 2)
|
||||
#expect(context.viewState.membersWithRole.first?.id == RoomMemberProxyMock.mockAdmin.userID)
|
||||
#expect(!context.viewState.hasChanges)
|
||||
#expect(!context.viewState.isSearching)
|
||||
}
|
||||
|
||||
func testToggleUserOn() {
|
||||
testInitialStateModerators()
|
||||
guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }) else {
|
||||
XCTFail("There should be a regular user available to promote.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func initialStateModerators() {
|
||||
setup(mode: .moderator)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.administrators == context.viewState.visibleAdministrators)
|
||||
#expect(context.viewState.moderators == context.viewState.visibleModerators)
|
||||
#expect(context.viewState.users == context.viewState.visibleUsers)
|
||||
#expect(context.viewState.membersWithRole.count == 3)
|
||||
#expect(context.viewState.membersWithRole.first { $0.id == RoomMemberProxyMock.mockModerator.userID } != nil)
|
||||
#expect(!context.viewState.hasChanges)
|
||||
#expect(!context.viewState.isSearching)
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func toggleUserOn() throws {
|
||||
setup(mode: .moderator)
|
||||
let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
|
||||
"There should be a regular user available to promote.")
|
||||
|
||||
context.send(viewAction: .toggleMember(firstUser))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [firstUser])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 4)
|
||||
XCTAssertTrue(context.viewState.membersWithRole.contains(firstUser))
|
||||
XCTAssertTrue(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [firstUser])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.membersWithRole.count == 4)
|
||||
#expect(context.viewState.membersWithRole.contains(firstUser))
|
||||
#expect(context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testToggleUserOff() {
|
||||
testToggleUserOn()
|
||||
guard let firstUser = context.viewState.membersToPromote.first else {
|
||||
XCTFail("There should be a promoted member before we begin.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func toggleUserOff() throws {
|
||||
try toggleUserOn()
|
||||
let firstUser = try #require(context.viewState.membersToPromote.first,
|
||||
"There should be a regular user available to promote.")
|
||||
|
||||
// Then toggle off
|
||||
context.send(viewAction: .toggleMember(firstUser))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 3)
|
||||
XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser))
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.membersWithRole.count == 3)
|
||||
#expect(!context.viewState.membersWithRole.contains(firstUser))
|
||||
#expect(!context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testDemoteToggledUser() {
|
||||
testToggleUserOn()
|
||||
guard let firstUser = context.viewState.membersToPromote.first else {
|
||||
XCTFail("There should be a promoted member before we begin.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func demoteToggledUser() throws {
|
||||
try toggleUserOn()
|
||||
let firstUser = try #require(context.viewState.membersToPromote.first,
|
||||
"There should be a regular user available to promote.")
|
||||
|
||||
// Then demote
|
||||
context.send(viewAction: .demoteMember(firstUser))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 3)
|
||||
XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser))
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.membersWithRole.count == 3)
|
||||
#expect(!context.viewState.membersWithRole.contains(firstUser))
|
||||
#expect(!context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testToggleModeratorOff() {
|
||||
testInitialStateModerators()
|
||||
guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else {
|
||||
XCTFail("There should be a member with the role before we begin.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func toggleModeratorOff() throws {
|
||||
initialStateModerators()
|
||||
let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
|
||||
"There should be a member with the role before we begin.")
|
||||
|
||||
context.send(viewAction: .toggleMember(existingModerator))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [existingModerator])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 2)
|
||||
XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator))
|
||||
XCTAssertTrue(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [existingModerator])
|
||||
#expect(context.viewState.membersWithRole.count == 2)
|
||||
#expect(!context.viewState.membersWithRole.contains(existingModerator))
|
||||
#expect(context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testToggleModeratorOn() {
|
||||
testToggleModeratorOff()
|
||||
|
||||
guard let demotedMember = context.viewState.membersToDemote.first else {
|
||||
XCTFail("There should be a member selected to demote before we begin.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func toggleModeratorOn() throws {
|
||||
try toggleModeratorOff()
|
||||
let demotedMember = try #require(context.viewState.membersToDemote.first,
|
||||
"There should be a member with the role before we begin.")
|
||||
|
||||
// Then toggle back on
|
||||
context.send(viewAction: .toggleMember(demotedMember))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 3)
|
||||
XCTAssertTrue(context.viewState.membersWithRole.contains(demotedMember))
|
||||
XCTAssertFalse(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [])
|
||||
#expect(context.viewState.membersWithRole.count == 3)
|
||||
#expect(context.viewState.membersWithRole.contains(demotedMember))
|
||||
#expect(!context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testDemoteModerator() {
|
||||
testInitialStateModerators()
|
||||
guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else {
|
||||
XCTFail("There should be a member with the role before we begin.")
|
||||
return
|
||||
}
|
||||
@Test
|
||||
mutating func demoteModerator() throws {
|
||||
initialStateModerators()
|
||||
let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
|
||||
"There should be a member with the role before we begin.")
|
||||
|
||||
context.send(viewAction: .demoteMember(existingModerator))
|
||||
|
||||
XCTAssertEqual(context.viewState.membersToPromote, [])
|
||||
XCTAssertEqual(context.viewState.membersToDemote, [existingModerator])
|
||||
XCTAssertEqual(context.viewState.membersWithRole.count, 2)
|
||||
XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator))
|
||||
XCTAssertTrue(context.viewState.hasChanges)
|
||||
#expect(context.viewState.membersToPromote == [])
|
||||
#expect(context.viewState.membersToDemote == [existingModerator])
|
||||
#expect(context.viewState.membersWithRole.count == 2)
|
||||
#expect(!context.viewState.membersWithRole.contains(existingModerator))
|
||||
#expect(context.viewState.hasChanges)
|
||||
}
|
||||
|
||||
func testSaveModeratorChanges() async throws {
|
||||
@Test
|
||||
mutating func saveModeratorChanges() async throws {
|
||||
// Given the change roles view model for moderators.
|
||||
setupViewModel(mode: .moderator)
|
||||
setup(mode: .moderator)
|
||||
|
||||
guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }),
|
||||
let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else {
|
||||
XCTFail("There should be a regular user and a moderator to begin with.")
|
||||
return
|
||||
}
|
||||
let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
|
||||
"There should be a regular user to begin with.")
|
||||
let existingModerator = try #require(context.viewState.membersWithRole.first { $0.role == .moderator },
|
||||
"There should be a moderator to begin with.")
|
||||
|
||||
// When promoting a regular user and demoting a moderator.
|
||||
context.send(viewAction: .toggleMember(firstUser))
|
||||
@@ -159,40 +158,41 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then no warning should be shown, and the call to update the users should be made straight away.
|
||||
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 2)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == existingModerator.id && $0.powerLevel == 0 }, true)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 50 }, true)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count == 2)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == existingModerator.id && $0.powerLevel == 0 } == true)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 50 } == true)
|
||||
}
|
||||
|
||||
func testSavePromotedAdministrator() async throws {
|
||||
@Test
|
||||
mutating func savePromotedAdministrator() async throws {
|
||||
// Given the change roles view model for administrators.
|
||||
setupViewModel(mode: .administrator)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
setup(mode: .administrator)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }) else {
|
||||
XCTFail("There should be a regular user to begin with.")
|
||||
return
|
||||
}
|
||||
let firstUser = try #require(context.viewState.users.first { !context.viewState.isMemberSelected($0) },
|
||||
"There should be a regular user to begin with.")
|
||||
|
||||
// When saving changes to promote a user to an administrator.
|
||||
context.send(viewAction: .toggleMember(firstUser))
|
||||
context.send(viewAction: .save)
|
||||
|
||||
// Then an alert should be shown to warn the action cannot be undone.
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
// When confirming the prompt
|
||||
context.alertInfo?.primaryButton.action?()
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
// Then the user should be made into an administrator.
|
||||
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count, 1)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 }, true)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.count == 1)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 } == true)
|
||||
}
|
||||
|
||||
private func setupViewModel(mode: RoomRole) {
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(mode: RoomRole) {
|
||||
roomProxy = JoinedRoomProxyMock(.init(members: .allMembersAsAdmin))
|
||||
viewModel = RoomChangeRolesScreenViewModel(mode: mode,
|
||||
roomProxy: roomProxy,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class RoomDirectorySearchScreenScreenViewModelTests: XCTestCase { }
|
||||
@@ -8,13 +8,14 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RoomEventStringBuilderTests: XCTestCase {
|
||||
var ownUserID: String!
|
||||
var stringBuilder: RoomEventStringBuilder!
|
||||
@Suite
|
||||
struct RoomEventStringBuilderTests {
|
||||
private let ownUserID: String
|
||||
private let stringBuilder: RoomEventStringBuilder
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
ownUserID = "@alice:matrix.org"
|
||||
let stateEventStringBuilder = RoomStateEventStringBuilder(userID: ownUserID)
|
||||
let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||
@@ -26,36 +27,37 @@ class RoomEventStringBuilderTests: XCTestCase {
|
||||
shouldPrefixSenderName: true)
|
||||
}
|
||||
|
||||
func testSenderPrefix() {
|
||||
@Test
|
||||
func senderPrefix() {
|
||||
let ownMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID, senderDisplayName: "Alice"))
|
||||
XCTAssertEqual(ownMessageString?.string, "You: Hello, World!", "Your own messages should be prefixed with 'You'")
|
||||
#expect(ownMessageString?.string == "You: Hello, World!", "Your own messages should be prefixed with 'You'")
|
||||
|
||||
let otherMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@bob:matrix.org", senderDisplayName: "Bob"))
|
||||
XCTAssertEqual(otherMessageString?.string, "Bob: Hello, World!", "Everyone else's messages should be prefixed with their display name.")
|
||||
#expect(otherMessageString?.string == "Bob: Hello, World!", "Everyone else's messages should be prefixed with their display name.")
|
||||
|
||||
let ambiguousMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@charlie:matrix.org",
|
||||
senderDisplayName: "Charlie",
|
||||
senderDisplayNameAmbiguous: true))
|
||||
XCTAssertEqual(ambiguousMessageString?.string, "Charlie (@charlie:matrix.org): Hello, World!",
|
||||
"Messages from senders with ambiguous display names should include their user ID in the prefix.")
|
||||
#expect(ambiguousMessageString?.string == "Charlie (@charlie:matrix.org): Hello, World!",
|
||||
"Messages from senders with ambiguous display names should include their user ID in the prefix.")
|
||||
|
||||
let ownEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID,
|
||||
senderDisplayName: "Alice",
|
||||
type: .emote,
|
||||
message: "laughs"))
|
||||
XCTAssertEqual(ownEmoteString?.string, "* Alice laughs", "Your own emotes shouldn't contain 'You'")
|
||||
#expect(ownEmoteString?.string == "* Alice laughs", "Your own emotes shouldn't contain 'You'")
|
||||
|
||||
let otherEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@bob:matrix.org",
|
||||
senderDisplayName: "Bob",
|
||||
type: .emote,
|
||||
message: "sighs"))
|
||||
XCTAssertEqual(otherEmoteString?.string, "* Bob sighs", "Everyone else's emotes should contain their display name.")
|
||||
#expect(otherEmoteString?.string == "* Bob sighs", "Everyone else's emotes should contain their display name.")
|
||||
|
||||
let ownPollString = stringBuilder.buildAttributedString(for: makePollItem(senderID: ownUserID, senderDisplayName: "Alice"))
|
||||
XCTAssertEqual(ownPollString?.string, "You: Poll: Which is better?", "Your own polls should be prefixed with 'You'")
|
||||
#expect(ownPollString?.string == "You: Poll: Which is better?", "Your own polls should be prefixed with 'You'")
|
||||
|
||||
let otherPollString = stringBuilder.buildAttributedString(for: makePollItem(senderID: "@bob:matrix.org", senderDisplayName: "Bob"))
|
||||
XCTAssertEqual(otherPollString?.string, "Bob: Poll: Which is better?", "Everyone else's polls should be prefixed with their display name.")
|
||||
#expect(otherPollString?.string == "Bob: Poll: Which is better?", "Everyone else's polls should be prefixed with their display name.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -7,116 +7,122 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
final class RoomListFiltersStateTests: XCTestCase {
|
||||
var appSettings: AppSettings!
|
||||
@Suite
|
||||
final class RoomListFiltersStateTests {
|
||||
var appSettings: AppSettings
|
||||
var state: RoomListFiltersState
|
||||
let allCasesWithoutLowPriority = RoomListFilter.allCases.filter { $0 != .lowPriority }
|
||||
|
||||
var state: RoomListFiltersState!
|
||||
var allCasesWithoutLowPriority = RoomListFilter.allCases.filter { $0 != .lowPriority }
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
state = RoomListFiltersState(appSettings: appSettings)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority)
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(!state.isFiltering)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == allCasesWithoutLowPriority)
|
||||
}
|
||||
|
||||
func testSetAndUnsetFilters() {
|
||||
@Test
|
||||
func setAndUnsetFilters() {
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.people, .rooms, .favourites])
|
||||
#expect(state.isFiltering)
|
||||
#expect(state.activeFilters == [.unreads])
|
||||
#expect(state.availableFilters == [.people, .rooms, .favourites])
|
||||
state.deactivateFilter(.unreads)
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority)
|
||||
#expect(!state.isFiltering)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == allCasesWithoutLowPriority)
|
||||
}
|
||||
|
||||
func testMutuallyExclusiveFilters() {
|
||||
@Test
|
||||
func mutuallyExclusiveFilters() {
|
||||
state.activateFilter(.people)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.people])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
#expect(state.isFiltering)
|
||||
#expect(state.activeFilters == [.people])
|
||||
#expect(state.availableFilters == [.unreads, .favourites])
|
||||
|
||||
state.deactivateFilter(.people)
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority)
|
||||
#expect(!state.isFiltering)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == allCasesWithoutLowPriority)
|
||||
|
||||
state.activateFilter(.rooms)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
#expect(state.isFiltering)
|
||||
#expect(state.activeFilters == [.rooms])
|
||||
#expect(state.availableFilters == [.unreads, .favourites])
|
||||
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertTrue(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms, .unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.favourites])
|
||||
#expect(state.isFiltering)
|
||||
#expect(state.activeFilters == [.rooms, .unreads])
|
||||
#expect(state.availableFilters == [.favourites])
|
||||
}
|
||||
|
||||
func testClearFilters() {
|
||||
@Test
|
||||
func clearFilters() {
|
||||
state.activateFilter(.people)
|
||||
XCTAssertEqual(state.activeFilters, [.people])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
#expect(state.activeFilters == [.people])
|
||||
#expect(state.availableFilters == [.unreads, .favourites])
|
||||
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertEqual(state.activeFilters, [.people, .unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.favourites])
|
||||
#expect(state.activeFilters == [.people, .unreads])
|
||||
#expect(state.availableFilters == [.favourites])
|
||||
|
||||
state.activateFilter(.favourites)
|
||||
XCTAssertEqual(state.activeFilters, [.people, .unreads, .favourites])
|
||||
XCTAssertEqual(state.availableFilters, [])
|
||||
#expect(state.activeFilters == [.people, .unreads, .favourites])
|
||||
#expect(state.availableFilters == [])
|
||||
|
||||
state.clearFilters()
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority)
|
||||
#expect(!state.isFiltering)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == allCasesWithoutLowPriority)
|
||||
}
|
||||
|
||||
func testOrder() {
|
||||
@Test
|
||||
func order() {
|
||||
state.activateFilter(.favourites)
|
||||
XCTAssertEqual(state.activeFilters, [.favourites])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .people, .rooms])
|
||||
#expect(state.activeFilters == [.favourites])
|
||||
#expect(state.availableFilters == [.unreads, .people, .rooms])
|
||||
|
||||
state.deactivateFilter(.favourites)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, allCasesWithoutLowPriority)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == allCasesWithoutLowPriority)
|
||||
|
||||
state.activateFilter(.rooms)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
#expect(state.activeFilters == [.rooms])
|
||||
#expect(state.availableFilters == [.unreads, .favourites])
|
||||
|
||||
state.activateFilter(.unreads)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms, .unreads])
|
||||
XCTAssertEqual(state.availableFilters, [.favourites])
|
||||
#expect(state.activeFilters == [.rooms, .unreads])
|
||||
#expect(state.availableFilters == [.favourites])
|
||||
|
||||
state.deactivateFilter(.unreads)
|
||||
XCTAssertEqual(state.activeFilters, [.rooms])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
|
||||
#expect(state.activeFilters == [.rooms])
|
||||
#expect(state.availableFilters == [.unreads, .favourites])
|
||||
}
|
||||
|
||||
// MARK: Low Priority feature flag
|
||||
|
||||
/// Don't forget to add .lowPriority into the mix above when enabling the feature.
|
||||
func testWithLowPriorityFeature() {
|
||||
@Test
|
||||
func withLowPriorityFeature() {
|
||||
enableLowPriorityFeature()
|
||||
XCTAssertFalse(state.isFiltering)
|
||||
XCTAssertEqual(state.activeFilters, [])
|
||||
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
|
||||
#expect(!state.isFiltering)
|
||||
#expect(state.activeFilters == [])
|
||||
#expect(state.availableFilters == RoomListFilter.allCases)
|
||||
|
||||
state.activateFilter(.lowPriority)
|
||||
XCTAssertEqual(state.activeFilters, [.lowPriority])
|
||||
XCTAssertEqual(state.availableFilters, [.unreads, .people, .rooms])
|
||||
#expect(state.activeFilters == [.lowPriority])
|
||||
#expect(state.availableFilters == [.unreads, .people, .rooms])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomMemberDetailsViewModelTests {
|
||||
var viewModel: RoomMemberDetailsScreenViewModelProtocol!
|
||||
var roomProxyMock: JoinedRoomProxyMock!
|
||||
var roomMemberProxyMock: RoomMemberProxyMock!
|
||||
@@ -18,202 +19,154 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() async throws {
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
|
||||
|
||||
roomProxyMock.getMemberUserIDClosure = { _ in
|
||||
.success(self.roomMemberProxyMock)
|
||||
}
|
||||
}
|
||||
|
||||
func testInitialState() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
@Test
|
||||
mutating func initialState() async throws {
|
||||
setup(roomMemberProxyMock: .mockAlice)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
XCTAssertNil(context.ignoreUserAlert)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
|
||||
#expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
#expect(context.ignoreUserAlert == nil)
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testIgnoreSuccess() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
@Test
|
||||
mutating func ignoreSuccess() async throws {
|
||||
setup(roomMemberProxyMock: .mockAlice)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
|
||||
context.send(viewAction: .showIgnoreAlert)
|
||||
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore))
|
||||
|
||||
#expect(context.ignoreUserAlert == .init(action: .ignore))
|
||||
context.send(viewAction: .ignoreConfirmed)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.memberDetails?.isIgnored == true
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let memberDetails = context.viewState.memberDetails else {
|
||||
XCTFail("Member details should be loaded at this point")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(memberDetails.isIgnored)
|
||||
|
||||
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
|
||||
|
||||
let memberDetails = try #require(context.viewState.memberDetails,
|
||||
"Member details should be loaded at this point")
|
||||
#expect(memberDetails.isIgnored)
|
||||
#expect(!context.viewState.isProcessingIgnoreRequest)
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(roomProxyMock.updateMembersCalled)
|
||||
#expect(roomProxyMock.updateMembersCalled)
|
||||
}
|
||||
|
||||
func testIgnoreFailure() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
|
||||
@Test
|
||||
mutating func ignoreFailure() async throws {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.ignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
setup(roomMemberProxyMock: .mockAlice, clientProxy: clientProxy)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
context.send(viewAction: .showIgnoreAlert)
|
||||
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore))
|
||||
|
||||
#expect(context.ignoreUserAlert == .init(action: .ignore))
|
||||
context.send(viewAction: .ignoreConfirmed)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.bindings.alertInfo != nil
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let memberDetails = context.viewState.memberDetails else {
|
||||
XCTFail("Member details should be loaded at this point")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(memberDetails.isIgnored)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
let memberDetails = try #require(context.viewState.memberDetails,
|
||||
"Member details should be loaded at this point")
|
||||
#expect(!memberDetails.isIgnored)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertFalse(roomProxyMock.updateMembersCalled)
|
||||
#expect(!roomProxyMock.updateMembersCalled)
|
||||
}
|
||||
|
||||
func testUnignoreSuccess() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
|
||||
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
@Test
|
||||
mutating func unignoreSuccess() async throws {
|
||||
setup(roomMemberProxyMock: .mockIgnored)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
|
||||
context.send(viewAction: .showUnignoreAlert)
|
||||
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore))
|
||||
|
||||
#expect(context.ignoreUserAlert == .init(action: .unignore))
|
||||
context.send(viewAction: .unignoreConfirmed)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.memberDetails?.isIgnored == false
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let memberDetails = context.viewState.memberDetails else {
|
||||
XCTFail("Member details should be loaded at this point")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertFalse(memberDetails.isIgnored)
|
||||
let memberDetails = try #require(context.viewState.memberDetails,
|
||||
"Member details should be loaded at this point")
|
||||
#expect(!memberDetails.isIgnored)
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertTrue(roomProxyMock.updateMembersCalled)
|
||||
#expect(roomProxyMock.updateMembersCalled)
|
||||
}
|
||||
|
||||
func testUnignoreFailure() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
|
||||
@Test
|
||||
mutating func unignoreFailure() async throws {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.unignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
setup(roomMemberProxyMock: .mockIgnored, clientProxy: clientProxy)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
|
||||
context.send(viewAction: .showUnignoreAlert)
|
||||
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore))
|
||||
|
||||
#expect(context.ignoreUserAlert == .init(action: .unignore))
|
||||
context.send(viewAction: .unignoreConfirmed)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.bindings.alertInfo != nil
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
guard let memberDetails = context.viewState.memberDetails else {
|
||||
XCTFail("Member details should be loaded at this point")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(memberDetails.isIgnored)
|
||||
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
let memberDetails = try #require(context.viewState.memberDetails,
|
||||
"Member details should be loaded at this point")
|
||||
#expect(memberDetails.isIgnored)
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertFalse(roomProxyMock.updateMembersCalled)
|
||||
#expect(!roomProxyMock.updateMembersCalled)
|
||||
}
|
||||
|
||||
func testInitialStateAccountOwner() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockMe
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
@Test
|
||||
mutating func initialStateAccountOwner() async throws {
|
||||
setup(roomMemberProxyMock: .mockMe)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
XCTAssertNil(context.ignoreUserAlert)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
|
||||
#expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
#expect(context.ignoreUserAlert == nil)
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
func testInitialStateIgnoredUser() async throws {
|
||||
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
@Test
|
||||
mutating func initialStateIgnoredUser() async throws {
|
||||
setup(roomMemberProxyMock: .mockIgnored)
|
||||
|
||||
let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.memberDetails != nil }
|
||||
try await waitForMemberToLoad.fulfill()
|
||||
|
||||
#expect(context.viewState.memberDetails == RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
#expect(context.ignoreUserAlert == nil)
|
||||
#expect(context.alertInfo == nil)
|
||||
}
|
||||
|
||||
XCTAssertEqual(context.viewState.memberDetails, RoomMemberDetails(withProxy: roomMemberProxyMock))
|
||||
XCTAssertNil(context.ignoreUserAlert)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(roomMemberProxyMock: RoomMemberProxyMock, clientProxy: ClientProxyMock? = nil) {
|
||||
self.roomMemberProxyMock = roomMemberProxyMock
|
||||
roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
|
||||
roomProxyMock.getMemberUserIDClosure = { _ in
|
||||
.success(roomMemberProxyMock)
|
||||
}
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
let userSession = clientProxy != nil ? UserSessionMock(.init(clientProxy: clientProxy!)) : UserSessionMock(.init())
|
||||
viewModel = RoomMemberDetailsScreenViewModel(userID: roomMemberProxyMock.userID,
|
||||
roomProxy: roomProxyMock,
|
||||
userSession: userSession,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,24 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomMembersFlowCoordinatorTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomMembersFlowCoordinatorTests {
|
||||
var membersFlowCoordinator: RoomMembersFlowCoordinator!
|
||||
var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
var stateMachineFactory: PublishedStateMachineFactory!
|
||||
|
||||
func testClearRoute() async throws {
|
||||
try await setUp(entryPoint: .roomMembersList)
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomMembersListScreenCoordinator)
|
||||
|
||||
@Test
|
||||
mutating func clearRoute() async throws {
|
||||
try await setup(entryPoint: .roomMembersList)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomMembersListScreenCoordinator)
|
||||
|
||||
var membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMemberDetails(userID: "test", previousState: .roomMembersList) }
|
||||
membersFlowCoordinator.handleAppRoute(.roomMemberDetails(userID: "test"), animated: false)
|
||||
try await membersFlowStateExpectation.fulfill()
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is RoomMemberDetailsScreenCoordinator)
|
||||
|
||||
membersFlowStateExpectation = deferFulfillment(stateMachineFactory.membersFlowStatePublisher) { $0 == .roomMembersList }
|
||||
let membersFlowActionExpectation = deferFulfillment(membersFlowCoordinator.actions) { action in
|
||||
@@ -36,10 +38,12 @@ class RoomMembersFlowCoordinatorTests: XCTestCase {
|
||||
membersFlowCoordinator.clearRoute(animated: false)
|
||||
try await membersFlowStateExpectation.fulfill()
|
||||
try await membersFlowActionExpectation.fulfill()
|
||||
XCTAssertTrue(navigationStackCoordinator.stackCoordinators.last is BlankFormCoordinator)
|
||||
#expect(navigationStackCoordinator.stackCoordinators.last is BlankFormCoordinator)
|
||||
}
|
||||
|
||||
private func setUp(entryPoint: RoomMembersFlowCoordinatorEntryPoint) async throws {
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(entryPoint: RoomMembersFlowCoordinatorEntryPoint) async throws {
|
||||
stateMachineFactory = .init()
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(hideBrandChrome: false))
|
||||
@@ -47,7 +51,7 @@ class RoomMembersFlowCoordinatorTests: XCTestCase {
|
||||
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.directRoomForUserIDReturnValue = .success(nil)
|
||||
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
|
||||
@@ -8,44 +8,39 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomMembersListScreenViewModelTests {
|
||||
var viewModel: RoomMembersListScreenViewModel!
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
|
||||
var context: RoomMembersListScreenViewModel.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
viewModel = nil
|
||||
roomProxy = nil
|
||||
}
|
||||
|
||||
func testJoinedMembers() async throws {
|
||||
setup(with: [.mockAlice, .mockBob])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func joinedMembers() async throws {
|
||||
setup(members: [.mockAlice, .mockBob])
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleJoinedMembers.count == 2
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 2)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 2)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 2)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 2)
|
||||
}
|
||||
|
||||
func testSortingMembers() async throws {
|
||||
setup(with: [.mockModerator, .mockDan, .mockAlice, .mockAdmin])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func sortingMembers() async throws {
|
||||
setup(members: [.mockModerator, .mockDan, .mockAlice, .mockAdmin])
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleJoinedMembers.count == 4
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
|
||||
let sortedMembers: [RoomMemberListScreenEntry] = [
|
||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
|
||||
verificationState: .notVerified),
|
||||
@@ -56,247 +51,254 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
||||
verificationState: .notVerified)
|
||||
]
|
||||
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers)
|
||||
|
||||
#expect(viewModel.state.visibleJoinedMembers == sortedMembers)
|
||||
}
|
||||
|
||||
func testSearch() async throws {
|
||||
setup(with: [.mockAlice, .mockBob])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func search() async throws {
|
||||
setup(members: [.mockAlice, .mockBob])
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleJoinedMembers.count == 1
|
||||
}
|
||||
|
||||
context.searchQuery = "alice"
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 2)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 2)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 1)
|
||||
}
|
||||
|
||||
func testEmptySearch() async throws {
|
||||
setup(with: [.mockAlice, .mockBob])
|
||||
|
||||
@Test
|
||||
mutating func emptySearch() async throws {
|
||||
setup(members: [.mockAlice, .mockBob])
|
||||
|
||||
context.searchQuery = "WWW"
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.joinedMembersCount == 2
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 2)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 2)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 0)
|
||||
}
|
||||
|
||||
func testJoinedAndInvitedMembers() async throws {
|
||||
setup(with: [.mockInvited, .mockBob])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func joinedAndInvitedMembers() async throws {
|
||||
setup(members: [.mockInvited, .mockBob])
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleInvitedMembers.count == 1
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 1)
|
||||
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 1)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 1)
|
||||
#expect(viewModel.state.visibleInvitedMembers.count == 1)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 1)
|
||||
}
|
||||
|
||||
func testInvitedMembers() async throws {
|
||||
setup(with: [.mockInvited])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func invitedMembers() async throws {
|
||||
setup(members: [.mockInvited])
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleInvitedMembers.count == 1
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 0)
|
||||
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 0)
|
||||
#expect(viewModel.state.visibleInvitedMembers.count == 1)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 0)
|
||||
}
|
||||
|
||||
func testSearchInvitedMembers() async throws {
|
||||
setup(with: [.mockInvited])
|
||||
|
||||
|
||||
@Test
|
||||
mutating func searchInvitedMembers() async throws {
|
||||
setup(members: [.mockInvited])
|
||||
|
||||
context.searchQuery = "invited"
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { state in
|
||||
state.visibleInvitedMembers.count == 1
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
XCTAssertEqual(viewModel.state.joinedMembersCount, 0)
|
||||
XCTAssertEqual(viewModel.state.visibleInvitedMembers.count, 1)
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0)
|
||||
|
||||
#expect(viewModel.state.joinedMembersCount == 0)
|
||||
#expect(viewModel.state.visibleInvitedMembers.count == 1)
|
||||
#expect(viewModel.state.visibleJoinedMembers.count == 0)
|
||||
}
|
||||
|
||||
func testSelectUserAsUser() async throws {
|
||||
// Given the room list viewed as a regular user.
|
||||
setup(with: .allMembers)
|
||||
|
||||
@Test
|
||||
mutating func selectUserAsUser() async throws {
|
||||
setup(members: .allMembers)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
// When tapping on another user in the list.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
|
||||
XCTFail("Expected to find a regular user.")
|
||||
return
|
||||
}
|
||||
context.send(viewAction: .selectMember(user))
|
||||
|
||||
// Then the member's details should be shown.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertNotNil(context.manageMemeberViewModel)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, false)
|
||||
}
|
||||
|
||||
func testSelectUserAsAdmin() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
setup(with: .allMembersAsAdmin)
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
||||
try await deferred.fulfill()
|
||||
XCTAssertNil(context.manageMemeberViewModel)
|
||||
|
||||
// When tapping on a user in the list.
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
|
||||
XCTFail("Expected to find a regular user.")
|
||||
Issue.record("Expected to find a regular user.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(user))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then member management should be shown for that user.
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
|
||||
|
||||
#expect(context.manageMemeberViewModel != nil)
|
||||
#expect(context.manageMemeberViewModel?.state.memberDetails.id == user.id)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canKick == false)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canBan == false)
|
||||
}
|
||||
|
||||
func testSelectModeratorAsAdmin() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
setup(with: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func selectUserAsAdmin() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
||||
try await deferred.fulfill()
|
||||
XCTAssertNil(context.manageMemeberViewModel)
|
||||
|
||||
// When tapping on a moderator in the list.
|
||||
|
||||
#expect(context.manageMemeberViewModel == nil)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
|
||||
Issue.record("Expected to find a regular user.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(user))
|
||||
try await deferred.fulfill()
|
||||
|
||||
#expect(context.manageMemeberViewModel?.state.memberDetails.id == user.id)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isKickDisabled == false)
|
||||
#expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false)
|
||||
#expect(context.manageMemeberViewModel?.state.isMemberBanned == false)
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func selectModeratorAsAdmin() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
||||
try await deferred.fulfill()
|
||||
|
||||
#expect(context.manageMemeberViewModel == nil)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else {
|
||||
XCTFail("Expected to find a moderator.")
|
||||
Issue.record("Expected to find a moderator.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(moderator))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then member management should be shown for the moderator.
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, moderator.id)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
|
||||
|
||||
#expect(context.manageMemeberViewModel?.state.memberDetails.id == moderator.id)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isMemberBanned == false)
|
||||
#expect(context.manageMemeberViewModel?.state.isKickDisabled == false)
|
||||
#expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false)
|
||||
}
|
||||
|
||||
func testSelectAdminAsAdmin() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
setup(with: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func selectAdminAsAdmin() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
||||
try await deferred.fulfill()
|
||||
|
||||
// When tapping on another administrator in the list.
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role.isAdminOrHigher && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
|
||||
XCTFail("Expected to find another admin.")
|
||||
Issue.record("Expected to find another admin.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(admin))
|
||||
|
||||
// Then the administrator's details should be shown.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, admin.id)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
|
||||
|
||||
#expect(context.manageMemeberViewModel?.state.memberDetails.id == admin.id)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isKickDisabled == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isMemberBanned == false)
|
||||
}
|
||||
|
||||
func testSelectOwnMemberAsAdmin() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
setup(with: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func selectOwnMemberAsAdmin() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
// When tapping on yourself in the list.
|
||||
|
||||
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember }
|
||||
guard let ownMember = viewModel.state.visibleJoinedMembers.first(where: { $0.member.id == RoomMemberProxyMock.mockMe.userID })?.member else {
|
||||
XCTFail("Expected to find own user admin.")
|
||||
Issue.record("Expected to find own user admin.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(ownMember))
|
||||
|
||||
// Then your member's details should be shown.
|
||||
try await memberDetailsAction.fulfill()
|
||||
XCTAssertNil(context.manageMemeberViewModel)
|
||||
|
||||
#expect(context.manageMemeberViewModel == nil)
|
||||
}
|
||||
|
||||
func testSelectBannedMember() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
setup(with: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
|
||||
|
||||
@Test
|
||||
mutating func selectBannedMember() async throws {
|
||||
setup(members: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
|
||||
try await deferred.fulfill()
|
||||
XCTAssertNil(context.alertInfo)
|
||||
|
||||
// When tapping on a banned member in the list.
|
||||
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
|
||||
guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else {
|
||||
XCTFail("Expected to find a banned user.")
|
||||
Issue.record("Expected to find a banned user.")
|
||||
return
|
||||
}
|
||||
|
||||
context.send(viewAction: .selectMember(bannedMember))
|
||||
|
||||
// Then an alert should be shown to unban the user.
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, bannedMember.id)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
|
||||
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, true)
|
||||
|
||||
#expect(context.manageMemeberViewModel?.state.memberDetails.id == bannedMember.id)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canKick == true)
|
||||
#expect(context.manageMemeberViewModel?.state.permissions.canBan == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isKickDisabled == true)
|
||||
#expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false)
|
||||
#expect(context.manageMemeberViewModel?.state.isMemberBanned == true)
|
||||
}
|
||||
|
||||
func testSwitchesToMembersModeWhenThereAreNoBannedMembers() async throws {
|
||||
// Given the room list viewed as an admin.
|
||||
|
||||
@Test
|
||||
mutating func switchesToMembersModeWhenThereAreNoBannedMembers() async throws {
|
||||
roomProxy = JoinedRoomProxyMock(.init(name: "test"))
|
||||
|
||||
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([RoomMemberProxyMock].allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
|
||||
roomProxy.membersPublisher = subject.asCurrentValuePublisher()
|
||||
viewModel = .init(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
|
||||
viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
||||
let context = viewModel.context
|
||||
|
||||
var deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 4 && $0.bindings.mode == .banned }
|
||||
context.mode = .banned
|
||||
try await deferred.fulfill()
|
||||
|
||||
|
||||
deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 0 && $0.bindings.mode == .members }
|
||||
subject.value = [RoomMemberProxyMock].allMembersAsAdmin
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
private func setup(with members: [RoomMemberProxyMock]) {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(members: [RoomMemberProxyMock]) {
|
||||
roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
viewModel = .init(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()),
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RoomPermissionsTests: XCTestCase {
|
||||
func testFromRust() {
|
||||
@Suite
|
||||
struct RoomPermissionsTests {
|
||||
@Test
|
||||
func fromRust() {
|
||||
// Given a set of power level changes with various values.
|
||||
let powerLevels = RoomPowerLevelsValues(ban: 100,
|
||||
invite: 100,
|
||||
@@ -29,16 +31,16 @@ class RoomPermissionsTests: XCTestCase {
|
||||
let permissions = RoomPermissions(powerLevels: powerLevels)
|
||||
|
||||
// Then the permissions should be created with values mapped to the correct role.
|
||||
XCTAssertEqual(permissions.ban, RoomRole.administrator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.invite, RoomRole.administrator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.kick, RoomRole.administrator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.redact, RoomRole.moderator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.eventsDefault, RoomRole.moderator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.stateDefault, RoomRole.moderator.powerLevelValue)
|
||||
XCTAssertEqual(permissions.usersDefault, RoomRole.user.powerLevelValue)
|
||||
XCTAssertEqual(permissions.roomName, RoomRole.user.powerLevelValue)
|
||||
XCTAssertEqual(permissions.roomAvatar, RoomRole.user.powerLevelValue)
|
||||
XCTAssertEqual(permissions.roomTopic, RoomRole.user.powerLevelValue)
|
||||
XCTAssertEqual(permissions.spaceChild, RoomRole.administrator.powerLevelValue)
|
||||
#expect(permissions.ban == RoomRole.administrator.powerLevelValue)
|
||||
#expect(permissions.invite == RoomRole.administrator.powerLevelValue)
|
||||
#expect(permissions.kick == RoomRole.administrator.powerLevelValue)
|
||||
#expect(permissions.redact == RoomRole.moderator.powerLevelValue)
|
||||
#expect(permissions.eventsDefault == RoomRole.moderator.powerLevelValue)
|
||||
#expect(permissions.stateDefault == RoomRole.moderator.powerLevelValue)
|
||||
#expect(permissions.usersDefault == RoomRole.user.powerLevelValue)
|
||||
#expect(permissions.roomName == RoomRole.user.powerLevelValue)
|
||||
#expect(permissions.roomAvatar == RoomRole.user.powerLevelValue)
|
||||
#expect(permissions.roomTopic == RoomRole.user.powerLevelValue)
|
||||
#expect(permissions.spaceChild == RoomRole.administrator.powerLevelValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,73 +7,81 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomRolesAndPermissionsScreenViewModelTests {
|
||||
var viewModel: RoomRolesAndPermissionsScreenViewModelProtocol!
|
||||
var roomProxy: JoinedRoomProxyMock!
|
||||
|
||||
|
||||
var context: RoomRolesAndPermissionsScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testEmptyCounters() {
|
||||
setupViewModel(members: .allMembers)
|
||||
XCTAssertEqual(context.viewState.administratorCount, 0)
|
||||
XCTAssertEqual(context.viewState.moderatorCount, 0)
|
||||
@Test
|
||||
mutating func emptyCounters() {
|
||||
setup(members: .allMembers)
|
||||
|
||||
#expect(context.viewState.administratorCount == 0)
|
||||
#expect(context.viewState.moderatorCount == 0)
|
||||
}
|
||||
|
||||
func testFilledCounters() {
|
||||
setupViewModel(members: .allMembersAsAdmin)
|
||||
XCTAssertEqual(context.viewState.administratorCount, 2)
|
||||
XCTAssertEqual(context.viewState.moderatorCount, 1)
|
||||
@Test
|
||||
mutating func filledCounters() {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
#expect(context.viewState.administratorCount == 2)
|
||||
#expect(context.viewState.moderatorCount == 1)
|
||||
}
|
||||
|
||||
func testResetPermissions() async throws {
|
||||
setupViewModel(members: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func resetPermissions() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
context.send(viewAction: .reset)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
context.alertInfo?.primaryButton.action?()
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
XCTAssertTrue(roomProxy.resetPowerLevelsCalled)
|
||||
#expect(roomProxy.resetPowerLevelsCalled)
|
||||
}
|
||||
|
||||
func testDemoteToModerator() async throws {
|
||||
setupViewModel(members: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func demoteToModerator() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
context.send(viewAction: .editOwnUserRole)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?()
|
||||
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel,
|
||||
RoomRole.moderator.powerLevelValue)
|
||||
|
||||
#expect(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.moderator.powerLevelValue)
|
||||
}
|
||||
|
||||
func testDemoteToMember() async throws {
|
||||
setupViewModel(members: .allMembersAsAdmin)
|
||||
|
||||
@Test
|
||||
mutating func demoteToMember() async throws {
|
||||
setup(members: .allMembersAsAdmin)
|
||||
|
||||
context.send(viewAction: .editOwnUserRole)
|
||||
XCTAssertNotNil(context.alertInfo)
|
||||
|
||||
#expect(context.alertInfo != nil)
|
||||
|
||||
context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?()
|
||||
|
||||
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel,
|
||||
RoomRole.user.powerLevelValue)
|
||||
|
||||
#expect(roomProxy.updatePowerLevelsForUsersCalled)
|
||||
#expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.user.powerLevelValue)
|
||||
}
|
||||
|
||||
private func setupViewModel(members: [RoomMemberProxyMock]) {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setup(members: [RoomMemberProxyMock]) {
|
||||
roomProxy = JoinedRoomProxyMock(.init(members: members))
|
||||
viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
|
||||
@@ -8,20 +8,22 @@
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
var userID: String!
|
||||
var stringBuilder: RoomStateEventStringBuilder!
|
||||
@Suite
|
||||
struct RoomStateEventStringBuilderTests {
|
||||
private let userID: String
|
||||
private let stringBuilder: RoomStateEventStringBuilder
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
userID = "@alice:matrix.org"
|
||||
stringBuilder = RoomStateEventStringBuilder(userID: userID)
|
||||
}
|
||||
|
||||
// MARK: - User Profiles
|
||||
|
||||
func testDisplayNameChanges() {
|
||||
@Test
|
||||
func displayNameChanges() {
|
||||
// Changes by you.
|
||||
validateDisplayNameChange(senderID: userID, oldName: "Alice", newName: "Bob",
|
||||
expectedString: L10n.stateEventDisplayNameChangedFromByYou("Alice", "Bob"))
|
||||
@@ -40,7 +42,7 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
expectedString: L10n.stateEventDisplayNameSet(senderID, "Bob"))
|
||||
}
|
||||
|
||||
func validateDisplayNameChange(senderID: String, oldName: String?, newName: String?, expectedString: String) {
|
||||
private func validateDisplayNameChange(senderID: String, oldName: String?, newName: String?, expectedString: String) {
|
||||
let sender = TimelineItemSender(id: senderID, displayName: newName)
|
||||
let string = stringBuilder.buildProfileChangeString(displayName: newName,
|
||||
previousDisplayName: oldName,
|
||||
@@ -48,10 +50,11 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
previousAvatarURLString: nil,
|
||||
member: sender.id,
|
||||
memberIsYou: sender.id == userID)
|
||||
XCTAssertEqual(string, expectedString)
|
||||
#expect(string == expectedString)
|
||||
}
|
||||
|
||||
func testAvatarChanges() {
|
||||
@Test
|
||||
func avatarChanges() {
|
||||
// Changes by you.
|
||||
validateAvatarChange(senderID: userID, oldAvatarURL: "mxc://1", newAvatarURL: "mxc://2",
|
||||
expectedString: L10n.stateEventAvatarUrlChangedByYou)
|
||||
@@ -71,9 +74,9 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
expectedString: L10n.stateEventAvatarUrlChanged(senderName))
|
||||
}
|
||||
|
||||
func validateAvatarChange(senderID: String, senderName: String? = nil,
|
||||
oldAvatarURL: String?, newAvatarURL: String?,
|
||||
expectedString: String) {
|
||||
private func validateAvatarChange(senderID: String, senderName: String? = nil,
|
||||
oldAvatarURL: String?, newAvatarURL: String?,
|
||||
expectedString: String) {
|
||||
let sender = TimelineItemSender(id: senderID, displayName: senderName)
|
||||
let string = stringBuilder.buildProfileChangeString(displayName: senderName,
|
||||
previousDisplayName: senderName,
|
||||
@@ -81,36 +84,38 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
previousAvatarURLString: oldAvatarURL,
|
||||
member: sender.id,
|
||||
memberIsYou: sender.id == userID)
|
||||
XCTAssertEqual(string, expectedString)
|
||||
#expect(string == expectedString)
|
||||
}
|
||||
|
||||
// MARK: - Room Info
|
||||
|
||||
func testTopicChanges() {
|
||||
@Test
|
||||
func topicChanges() {
|
||||
let you = TimelineItemSender(id: userID, displayName: "Alice")
|
||||
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
|
||||
|
||||
let newTopic = "New topic"
|
||||
var string = stringBuilder.buildString(for: .roomTopic(topic: newTopic), sender: you, isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicChangedByYou(newTopic))
|
||||
#expect(string == L10n.stateEventRoomTopicChangedByYou(newTopic))
|
||||
string = stringBuilder.buildString(for: .roomTopic(topic: newTopic), sender: other, isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicChanged(other.displayName ?? "", newTopic))
|
||||
#expect(string == L10n.stateEventRoomTopicChanged(other.displayName ?? "", newTopic))
|
||||
|
||||
let emptyTopic = ""
|
||||
string = stringBuilder.buildString(for: .roomTopic(topic: emptyTopic), sender: you, isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicRemovedByYou)
|
||||
#expect(string == L10n.stateEventRoomTopicRemovedByYou)
|
||||
string = stringBuilder.buildString(for: .roomTopic(topic: emptyTopic), sender: other, isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicRemoved(other.displayName ?? ""))
|
||||
#expect(string == L10n.stateEventRoomTopicRemoved(other.displayName ?? ""))
|
||||
|
||||
string = stringBuilder.buildString(for: .roomTopic(topic: nil), sender: you, isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicRemovedByYou)
|
||||
#expect(string == L10n.stateEventRoomTopicRemovedByYou)
|
||||
string = stringBuilder.buildString(for: .roomTopic(topic: nil), sender: other, isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomTopicRemoved(other.displayName ?? ""))
|
||||
#expect(string == L10n.stateEventRoomTopicRemoved(other.displayName ?? ""))
|
||||
}
|
||||
|
||||
// MARK: - Room Membership
|
||||
|
||||
func testKickMember() {
|
||||
@Test
|
||||
func kickMember() {
|
||||
let you = TimelineItemSender(id: userID, displayName: "Alice")
|
||||
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
|
||||
let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam")
|
||||
@@ -122,31 +127,32 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: you,
|
||||
isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomRemoveByYouWithReason(banned.displayName ?? banned.id, reason))
|
||||
#expect(string == L10n.stateEventRoomRemoveByYouWithReason(banned.displayName ?? banned.id, reason))
|
||||
string = stringBuilder.buildString(for: .kicked,
|
||||
reason: nil,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: you,
|
||||
isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomRemoveByYou(banned.displayName ?? banned.id))
|
||||
#expect(string == L10n.stateEventRoomRemoveByYou(banned.displayName ?? banned.id))
|
||||
string = stringBuilder.buildString(for: .kicked,
|
||||
reason: reason,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: other,
|
||||
isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomRemoveWithReason(other.displayName ?? other.id, banned.displayName ?? banned.id, reason))
|
||||
#expect(string == L10n.stateEventRoomRemoveWithReason(other.displayName ?? other.id, banned.displayName ?? banned.id, reason))
|
||||
string = stringBuilder.buildString(for: .kicked,
|
||||
reason: nil,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: other,
|
||||
isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomRemove(other.displayName ?? other.id, banned.displayName ?? banned.id))
|
||||
#expect(string == L10n.stateEventRoomRemove(other.displayName ?? other.id, banned.displayName ?? banned.id))
|
||||
}
|
||||
|
||||
func testBanMember() {
|
||||
@Test
|
||||
func banMember() {
|
||||
let you = TimelineItemSender(id: userID, displayName: "Alice")
|
||||
let other = TimelineItemSender(id: "@bob:matrix.org", displayName: "Bob")
|
||||
let banned = TimelineItemSender(id: "@spam:matrix.org", displayName: "I like spam")
|
||||
@@ -158,27 +164,27 @@ class RoomStateEventStringBuilderTests: XCTestCase {
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: you,
|
||||
isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomBanByYouWithReason(banned.displayName ?? banned.id, reason))
|
||||
#expect(string == L10n.stateEventRoomBanByYouWithReason(banned.displayName ?? banned.id, reason))
|
||||
string = stringBuilder.buildString(for: .banned,
|
||||
reason: nil,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: you,
|
||||
isOutgoing: true)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomBanByYou(banned.displayName ?? banned.id))
|
||||
#expect(string == L10n.stateEventRoomBanByYou(banned.displayName ?? banned.id))
|
||||
string = stringBuilder.buildString(for: .banned,
|
||||
reason: reason,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: other,
|
||||
isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomBanWithReason(other.displayName ?? other.id, banned.displayName ?? banned.id, reason))
|
||||
#expect(string == L10n.stateEventRoomBanWithReason(other.displayName ?? other.id, banned.displayName ?? banned.id, reason))
|
||||
string = stringBuilder.buildString(for: .banned,
|
||||
reason: nil,
|
||||
memberUserID: banned.id,
|
||||
memberDisplayName: banned.displayName,
|
||||
sender: other,
|
||||
isOutgoing: false)
|
||||
XCTAssertEqual(string, L10n.stateEventRoomBan(other.displayName ?? other.id, banned.displayName ?? banned.id))
|
||||
#expect(string == L10n.stateEventRoomBan(other.displayName ?? other.id, banned.displayName ?? banned.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,102 +9,96 @@
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
@MainActor
|
||||
final class RoomSummaryProviderTests {
|
||||
private let baseFilters: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]),
|
||||
.all(filters: [.space, .invite])]),
|
||||
.deduplicateVersions]
|
||||
|
||||
final class RoomSummaryProviderTests: XCTestCase {
|
||||
var appSettings: AppSettings!
|
||||
var roomList: RoomListSDKMock!
|
||||
var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock!
|
||||
|
||||
let baseFilters: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]),
|
||||
.all(filters: [.space, .invite])]),
|
||||
.deduplicateVersions]
|
||||
|
||||
var roomSummaryProvider: RoomSummaryProvider!
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testDefaultRustFilters() async {
|
||||
|
||||
@Test
|
||||
func defaultRustFilters() async {
|
||||
// Given a new room provider.
|
||||
setupProvider()
|
||||
setup()
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then it should have the default Rust filters enabled.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: baseFilters))
|
||||
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 1)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters))
|
||||
|
||||
// When setting one our user filters.
|
||||
roomSummaryProvider.setFilter(.all(filters: [.favourites]))
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then that filter should be added to the default Rust filters.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: [.all(filters: [.favourite, .joined])] + baseFilters))
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 2)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters))
|
||||
}
|
||||
|
||||
func testLowPriorityRustFilters() async {
|
||||
|
||||
@Test
|
||||
func lowPriorityRustFilters() async {
|
||||
// Given a new room provider with the low priority filter enabled.
|
||||
setupProvider(isLowPriorityFilterEnabled: true)
|
||||
setup(isLowPriorityFilterEnabled: true)
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then the default Rust filters should include the non-low priority filter,
|
||||
// so that low priority rooms are hidden from the top of the room list.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: baseFilters + [.nonLowPriority]))
|
||||
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 1)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters + [.nonLowPriority]))
|
||||
|
||||
// When setting the low priority filter.
|
||||
roomSummaryProvider.setFilter(.all(filters: [.lowPriority]))
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then the non-low priority filter should be replaced with the low priority filter.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters))
|
||||
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 2)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters))
|
||||
|
||||
// When setting another one of our filters.
|
||||
roomSummaryProvider.setFilter(.all(filters: [.rooms]))
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then the filter should be combined with the non-low priority filter.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 3)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority]))
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 3)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority]))
|
||||
}
|
||||
|
||||
func testRoomIdentifierFilters() async {
|
||||
setupProvider()
|
||||
|
||||
@Test
|
||||
func roomIdentifierFilters() async {
|
||||
setup()
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then it should have the default Rust filters enabled.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 1)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: baseFilters))
|
||||
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 1)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters))
|
||||
|
||||
// When setting one our user filters.
|
||||
roomSummaryProvider.setFilter(.rooms(roomsIDs: ["SomeRoom"], filters: [.favourites]))
|
||||
await Task.yield()
|
||||
|
||||
|
||||
// Then that filter should be added to the default Rust filters.
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindCallsCount, 2)
|
||||
XCTAssertEqual(dynamicEntriesController.setFilterKindReceivedInvocations.last,
|
||||
.all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])]))
|
||||
#expect(dynamicEntriesController.setFilterKindCallsCount == 2)
|
||||
#expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])]))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupProvider(isLowPriorityFilterEnabled: Bool = false) {
|
||||
private func setup(isLowPriorityFilterEnabled: Bool = false) {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
appSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled
|
||||
|
||||
|
||||
let stateEventStringBuilder = RoomStateEventStringBuilder(userID: "@me:matrix.org")
|
||||
let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: stateEventStringBuilder,
|
||||
@@ -112,13 +106,13 @@ final class RoomSummaryProviderTests: XCTestCase {
|
||||
destination: .roomList),
|
||||
shouldDisambiguateDisplayNames: true,
|
||||
shouldPrefixSenderName: true)
|
||||
|
||||
|
||||
roomSummaryProvider = RoomSummaryProvider(roomListService: RoomListServiceSDKMock(),
|
||||
eventStringBuilder: eventStringBuilder,
|
||||
name: "Test",
|
||||
notificationSettings: NotificationSettingsProxyMock(with: .init()),
|
||||
appSettings: appSettings)
|
||||
|
||||
|
||||
dynamicEntriesController = RoomListDynamicEntriesControllerSDKMock()
|
||||
dynamicEntriesController.setFilterKindReturnValue = true
|
||||
let dynamicAdaptersResult = RoomListEntriesWithDynamicAdaptersResultSDKMock()
|
||||
|
||||
@@ -7,83 +7,90 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class RoomSummaryTests: XCTestCase {
|
||||
@Suite
|
||||
struct RoomSummaryTests {
|
||||
// swiftlint:disable:next large_tuple
|
||||
let roomDetails: (id: String, name: String, avatarURL: URL) = ("room_id", "Room Name", "mxc://hs.tld/room/avatar")
|
||||
let heroes = [UserProfileProxy(userID: "hero_1", displayName: "Hero 1", avatarURL: "mxc://hs.tld/user/avatar")]
|
||||
|
||||
func testRoomAvatar() {
|
||||
@Test
|
||||
func roomAvatar() {
|
||||
let details = makeSummary(isDirect: false, isSpace: false, hasRoomAvatar: true, isTombstoned: false)
|
||||
|
||||
switch details.avatar {
|
||||
case .room(let id, let name, let avatarURL):
|
||||
XCTAssertEqual(id, roomDetails.id)
|
||||
XCTAssertEqual(name, roomDetails.name)
|
||||
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
||||
#expect(id == roomDetails.id)
|
||||
#expect(name == roomDetails.name)
|
||||
#expect(avatarURL == roomDetails.avatarURL)
|
||||
case .heroes:
|
||||
XCTFail("A room shouldn't use the heroes for its avatar.")
|
||||
Issue.record("A room shouldn't use the heroes for its avatar.")
|
||||
case .space:
|
||||
XCTFail("A room shouldn't use a space avatar.")
|
||||
Issue.record("A room shouldn't use a space avatar.")
|
||||
case .tombstoned:
|
||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||
Issue.record("A room shouldn't use the tombstone for its avatar.")
|
||||
}
|
||||
}
|
||||
|
||||
func testDMAvatarSet() {
|
||||
@Test
|
||||
func dmAvatarSet() {
|
||||
let details = makeSummary(isDirect: true, isSpace: false, hasRoomAvatar: true, isTombstoned: false)
|
||||
|
||||
switch details.avatar {
|
||||
case .room(let id, let name, let avatarURL):
|
||||
XCTAssertEqual(id, roomDetails.id)
|
||||
XCTAssertEqual(name, roomDetails.name)
|
||||
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
||||
#expect(id == roomDetails.id)
|
||||
#expect(name == roomDetails.name)
|
||||
#expect(avatarURL == roomDetails.avatarURL)
|
||||
case .heroes:
|
||||
XCTFail("A DM with an avatar set shouldn't use the heroes instead.")
|
||||
Issue.record("A DM with an avatar set shouldn't use the heroes instead.")
|
||||
case .space:
|
||||
XCTFail("A DM shouldn't use a space avatar.")
|
||||
Issue.record("A DM shouldn't use a space avatar.")
|
||||
case .tombstoned:
|
||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||
Issue.record("A room shouldn't use the tombstone for its avatar.")
|
||||
}
|
||||
}
|
||||
|
||||
func testDMAvatarNotSet() {
|
||||
@Test
|
||||
func dmAvatarNotSet() {
|
||||
let details = makeSummary(isDirect: true, isSpace: false, hasRoomAvatar: false, isTombstoned: false)
|
||||
|
||||
switch details.avatar {
|
||||
case .room:
|
||||
XCTFail("A DM without an avatar should defer to the hero for the correct placeholder tint colour.")
|
||||
Issue.record("A DM without an avatar should defer to the hero for the correct placeholder tint colour.")
|
||||
case .heroes(let heroes):
|
||||
XCTAssertEqual(heroes, self.heroes)
|
||||
#expect(heroes == self.heroes)
|
||||
case .space:
|
||||
XCTFail("A DM shouldn't use a space avatar.")
|
||||
Issue.record("A DM shouldn't use a space avatar.")
|
||||
case .tombstoned:
|
||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||
Issue.record("A room shouldn't use the tombstone for its avatar.")
|
||||
}
|
||||
}
|
||||
|
||||
func testSpaceAvatar() {
|
||||
@Test
|
||||
func spaceAvatar() {
|
||||
let details = makeSummary(isDirect: false, isSpace: true, hasRoomAvatar: true, isTombstoned: false)
|
||||
|
||||
switch details.avatar {
|
||||
case .room:
|
||||
XCTFail("A space shouldn't use a room avatar.")
|
||||
Issue.record("A space shouldn't use a room avatar.")
|
||||
case .heroes:
|
||||
XCTFail("A room shouldn't use the heroes for its avatar.")
|
||||
Issue.record("A room shouldn't use the heroes for its avatar.")
|
||||
case .space(let id, let name, let avatarURL):
|
||||
XCTAssertEqual(id, roomDetails.id)
|
||||
XCTAssertEqual(name, roomDetails.name)
|
||||
XCTAssertEqual(avatarURL, roomDetails.avatarURL)
|
||||
#expect(id == roomDetails.id)
|
||||
#expect(name == roomDetails.name)
|
||||
#expect(avatarURL == roomDetails.avatarURL)
|
||||
case .tombstoned:
|
||||
XCTFail("A room shouldn't use the tombstone for its avatar.")
|
||||
Issue.record("A room shouldn't use the tombstone for its avatar.")
|
||||
}
|
||||
}
|
||||
|
||||
func testTombstonedAvatar() {
|
||||
@Test
|
||||
func tombstonedAvatar() {
|
||||
let details = makeSummary(isDirect: false, isSpace: false, hasRoomAvatar: true, isTombstoned: true)
|
||||
|
||||
XCTAssertEqual(details.avatar, .tombstoned)
|
||||
#expect(details.avatar == .tombstoned)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -9,27 +9,29 @@
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import MatrixRustSDKMocks
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
class RoomTests: XCTestCase {
|
||||
func testCallIntent() async {
|
||||
@Suite
|
||||
struct RoomTests {
|
||||
@Test
|
||||
func callIntent() async {
|
||||
let room = RoomSDKMock()
|
||||
room.hasActiveRoomCallReturnValue = false
|
||||
room.isDirectReturnValue = false
|
||||
|
||||
var callIntent = await room.joinCallIntent
|
||||
XCTAssertEqual(callIntent, .startCall)
|
||||
#expect(callIntent == .startCall)
|
||||
|
||||
room.isDirectReturnValue = true
|
||||
callIntent = await room.joinCallIntent
|
||||
XCTAssertEqual(callIntent, .startCallDm)
|
||||
#expect(callIntent == .startCallDm)
|
||||
|
||||
room.hasActiveRoomCallReturnValue = true
|
||||
callIntent = await room.joinCallIntent
|
||||
XCTAssertEqual(callIntent, .joinExistingDm)
|
||||
#expect(callIntent == .joinExistingDm)
|
||||
|
||||
room.isDirectReturnValue = false
|
||||
callIntent = await room.joinCallIntent
|
||||
XCTAssertEqual(callIntent, .joinExisting)
|
||||
#expect(callIntent == .joinExisting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class SecureBackupKeyBackupScreenViewModelTests: XCTestCase { }
|
||||
@@ -8,19 +8,20 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase {
|
||||
var viewModel: SecureBackupLogoutConfirmationScreenViewModel!
|
||||
var context: SecureBackupLogoutConfirmationScreenViewModel.Context {
|
||||
@Suite
|
||||
struct SecureBackupLogoutConfirmationScreenViewModelTests {
|
||||
private var viewModel: SecureBackupLogoutConfirmationScreenViewModel
|
||||
private var context: SecureBackupLogoutConfirmationScreenViewModel.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
var secureBackupController: SecureBackupControllerMock!
|
||||
var reachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never>!
|
||||
private var secureBackupController: SecureBackupControllerMock
|
||||
private var reachabilitySubject: CurrentValueSubject<NetworkMonitorReachability, Never>
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
secureBackupController = SecureBackupControllerMock()
|
||||
secureBackupController.underlyingKeyBackupState = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.enabled).asCurrentValuePublisher()
|
||||
|
||||
@@ -30,36 +31,57 @@ class SecureBackupLogoutConfirmationScreenViewModelTests: XCTestCase {
|
||||
homeserverReachabilityPublisher: reachabilitySubject.asCurrentValuePublisher())
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(context.viewState.mode, .saveRecoveryKey)
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(context.viewState.mode == .saveRecoveryKey)
|
||||
}
|
||||
|
||||
func testOngoingState() async throws {
|
||||
testInitialState()
|
||||
@Test
|
||||
func ongoingState() async throws {
|
||||
#expect(context.viewState.mode == .saveRecoveryKey)
|
||||
|
||||
let progressExpectation = expectation(description: "The upload progress callback should be called.")
|
||||
secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in
|
||||
try? await Task.sleep(for: .seconds(4))
|
||||
stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100))
|
||||
progressExpectation.fulfill()
|
||||
return .success(())
|
||||
try await confirmation { confirmation in
|
||||
secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in
|
||||
try? await Task.sleep(for: .seconds(4))
|
||||
stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100))
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let deferredWaiting = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: false) }
|
||||
context.send(viewAction: .logout)
|
||||
_ = try await deferredWaiting.fulfill()
|
||||
|
||||
// Wait for the 2-second timeout.
|
||||
let deferredHasStalled = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: true) }
|
||||
_ = try await deferredHasStalled.fulfill()
|
||||
|
||||
try await deferFulfillment(context.observe(\.viewState.mode)) { $0 == .backupOngoing(progress: 0.5) }.fulfill()
|
||||
}
|
||||
|
||||
let deferredWaiting = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: false) }
|
||||
context.send(viewAction: .logout)
|
||||
try await deferredWaiting.fulfill()
|
||||
|
||||
// Wait for the 2-second timeout.
|
||||
let deferredHasStalled = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: true) }
|
||||
try await deferredHasStalled.fulfill()
|
||||
|
||||
// Wait for the progress to be reported.
|
||||
await fulfillment(of: [progressExpectation])
|
||||
XCTAssertEqual(context.viewState.mode, .backupOngoing(progress: 0.5))
|
||||
}
|
||||
|
||||
func testOfflineState() async throws {
|
||||
try await testOngoingState()
|
||||
@Test
|
||||
func offlineState() async throws {
|
||||
#expect(context.viewState.mode == .saveRecoveryKey)
|
||||
|
||||
try await confirmation { confirmation in
|
||||
secureBackupController.waitForKeyBackupUploadUploadStateSubjectClosure = { stateSubject in
|
||||
try? await Task.sleep(for: .seconds(4))
|
||||
stateSubject.send(.uploading(uploadedKeyCount: 50, totalKeyCount: 100))
|
||||
confirmation()
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let deferredWaiting = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: false) }
|
||||
context.send(viewAction: .logout)
|
||||
try await deferredWaiting.fulfill()
|
||||
|
||||
// Wait for the 2-second timeout.
|
||||
let deferredHasStalled = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .waitingToStart(hasStalled: true) }
|
||||
try await deferredHasStalled.fulfill()
|
||||
|
||||
try await deferFulfillment(context.observe(\.viewState.mode)) { $0 == .backupOngoing(progress: 0.5) }.fulfill()
|
||||
}
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.viewState.mode)) { $0 == .offline }
|
||||
reachabilitySubject.send(.unreachable)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class SecureBackupRecoveryKeyScreenViewModelTests: XCTestCase { }
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
// Copyright 2022-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
class SecureBackupScreenViewModelTests: XCTestCase { }
|
||||
@@ -7,35 +7,38 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ServerConfirmationScreenViewStateTests: XCTestCase {
|
||||
func testLoginMessageString() {
|
||||
@Suite
|
||||
struct ServerConfirmationScreenViewStateTests {
|
||||
@Test
|
||||
func loginMessageString() {
|
||||
let matrixDotOrgLogin = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockMatrixDotOrg.address),
|
||||
authenticationFlow: .login)
|
||||
XCTAssertEqual(matrixDotOrgLogin.message, L10n.screenServerConfirmationMessageLoginMatrixDotOrg, "matrix.org should have a custom message.")
|
||||
#expect(matrixDotOrgLogin.message == L10n.screenServerConfirmationMessageLoginMatrixDotOrg, "matrix.org should have a custom message.")
|
||||
|
||||
let elementDotIoLogin = ServerConfirmationScreenViewState(mode: .confirmation("element.io"),
|
||||
authenticationFlow: .login)
|
||||
XCTAssertEqual(elementDotIoLogin.message, L10n.screenServerConfirmationMessageLoginElementDotIo, "element.io should have a custom message.")
|
||||
#expect(elementDotIoLogin.message == L10n.screenServerConfirmationMessageLoginElementDotIo, "element.io should have a custom message.")
|
||||
|
||||
let otherLogin = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockOIDC.address),
|
||||
authenticationFlow: .login)
|
||||
XCTAssertEqual(otherLogin.message, "", "Other servers should not show a message.")
|
||||
#expect(otherLogin.message == "", "Other servers should not show a message.")
|
||||
|
||||
let pickerLogin = ServerConfirmationScreenViewState(mode: .picker(["element.io", "matrix.org"]),
|
||||
authenticationFlow: .login)
|
||||
XCTAssertNil(pickerLogin.message, "The picker mode should not show a message.")
|
||||
#expect(pickerLogin.message == nil, "The picker mode should not show a message.")
|
||||
}
|
||||
|
||||
func testRegisterMessageString() {
|
||||
@Test
|
||||
func registerMessageString() {
|
||||
let matrixDotOrgRegister = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockMatrixDotOrg.address),
|
||||
authenticationFlow: .register)
|
||||
XCTAssertEqual(matrixDotOrgRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
|
||||
#expect(matrixDotOrgRegister.message == L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
|
||||
|
||||
let oidcRegister = ServerConfirmationScreenViewState(mode: .confirmation(LoginHomeserver.mockOIDC.address),
|
||||
authenticationFlow: .register)
|
||||
XCTAssertEqual(oidcRegister.message, L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
|
||||
#expect(oidcRegister.message == L10n.screenServerConfirmationMessageRegister, "The registration message should always be the same.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,25 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
@Suite
|
||||
struct ServerSelectionScreenViewModelTests {
|
||||
var clientFactory: AuthenticationClientFactoryMock!
|
||||
var service: AuthenticationServiceProtocol!
|
||||
|
||||
var viewModel: ServerSelectionScreenViewModelProtocol!
|
||||
|
||||
var context: ServerSelectionScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testSelectForLogin() async throws {
|
||||
@Test
|
||||
mutating func selectForLogin() async throws {
|
||||
// Given a view model for login.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When selecting matrix.org.
|
||||
context.homeserverAddress = "matrix.org"
|
||||
@@ -32,16 +34,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should succeed.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
func testLoginNotSupportedAlert() async throws {
|
||||
@Test
|
||||
mutating func loginNotSupportedAlert() async throws {
|
||||
// Given a view model for login.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that doesn't support login.
|
||||
context.homeserverAddress = "server.net"
|
||||
@@ -50,15 +53,16 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert about not supporting registration.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .loginAlert)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .loginAlert)
|
||||
}
|
||||
|
||||
func testSelectForRegistration() async throws {
|
||||
@Test
|
||||
mutating func selectForRegistration() async throws {
|
||||
// Given a view model for registration.
|
||||
setupViewModel(authenticationFlow: .register)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
setup(authenticationFlow: .register)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
|
||||
// When selecting matrix.org.
|
||||
context.homeserverAddress = "matrix.org"
|
||||
@@ -67,16 +71,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should succeed.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(service.homeserver.value, .mockMatrixDotOrg)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(service.homeserver.value == .mockMatrixDotOrg)
|
||||
}
|
||||
|
||||
func testRegistrationNotSupportedAlert() async throws {
|
||||
@Test
|
||||
mutating func registrationNotSupportedAlert() async throws {
|
||||
// Given a view model for registration.
|
||||
setupViewModel(authenticationFlow: .register)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
setup(authenticationFlow: .register)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that doesn't support registration.
|
||||
context.homeserverAddress = "example.com"
|
||||
@@ -85,16 +90,17 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert about not supporting registration.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .registrationAlert)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .registrationAlert)
|
||||
}
|
||||
|
||||
func testElementProRequiredAlert() async throws {
|
||||
@Test
|
||||
mutating func elementProRequiredAlert() async throws {
|
||||
// Given a view model for login.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertEqual(service.homeserver.value.loginMode, .unknown)
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 0)
|
||||
XCTAssertNil(context.alertInfo)
|
||||
setup(authenticationFlow: .login)
|
||||
#expect(service.homeserver.value.loginMode == .unknown)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 0)
|
||||
#expect(context.alertInfo == nil)
|
||||
|
||||
// When selecting a server that requires Element Pro
|
||||
context.homeserverAddress = "secure.gov"
|
||||
@@ -103,17 +109,18 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then selection should fail with an alert telling the user to download Element Pro.
|
||||
XCTAssertEqual(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount, 1)
|
||||
XCTAssertEqual(context.alertInfo?.id, .elementProAlert)
|
||||
#expect(clientFactory.makeClientHomeserverAddressSessionDirectoriesPassphraseClientSessionDelegateAppSettingsAppHooksCallsCount == 1)
|
||||
#expect(context.alertInfo?.id == .elementProAlert)
|
||||
}
|
||||
|
||||
func testInvalidServer() async throws {
|
||||
@Test
|
||||
mutating func invalidServer() async throws {
|
||||
// Given a new instance of the view model.
|
||||
setupViewModel(authenticationFlow: .login)
|
||||
XCTAssertFalse(context.viewState.isShowingFooterError, "There should not be an error message for a new view model.")
|
||||
XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.")
|
||||
XCTAssertEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice,
|
||||
"The standard footer message should be shown.")
|
||||
setup(authenticationFlow: .login)
|
||||
#expect(!context.viewState.isShowingFooterError, "There should not be an error message for a new view model.")
|
||||
#expect(context.viewState.footerErrorMessage == nil, "There should not be an error message for a new view model.")
|
||||
#expect(String(context.viewState.footerMessage) == L10n.screenChangeServerFormNotice,
|
||||
"The standard footer message should be shown.")
|
||||
|
||||
// When attempting to discover an invalid server
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { $0 }
|
||||
@@ -122,10 +129,10 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the footer should now be showing an error.
|
||||
XCTAssertTrue(context.viewState.isShowingFooterError, "The error message should be stored.")
|
||||
XCTAssertNotNil(context.viewState.footerErrorMessage, "The error message should be stored.")
|
||||
XCTAssertNotEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice,
|
||||
"The error message should be shown.")
|
||||
#expect(context.viewState.isShowingFooterError, "The error message should be stored.")
|
||||
#expect(context.viewState.footerErrorMessage != nil, "The error message should be stored.")
|
||||
#expect(String(context.viewState.footerMessage) != L10n.screenChangeServerFormNotice,
|
||||
"The error message should be shown.")
|
||||
|
||||
// And when clearing the error.
|
||||
deferred = deferFulfillment(context.observe(\.viewState.isShowingFooterError)) { !$0 }
|
||||
@@ -134,14 +141,14 @@ class ServerSelectionScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the error message should now be removed.
|
||||
XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.")
|
||||
XCTAssertEqual(String(context.viewState.footerMessage), L10n.screenChangeServerFormNotice,
|
||||
"The standard footer message should be shown again.")
|
||||
#expect(context.viewState.footerErrorMessage == nil, "The error message should have been cleared.")
|
||||
#expect(String(context.viewState.footerMessage) == L10n.screenChangeServerFormNotice,
|
||||
"The standard footer message should be shown again.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(authenticationFlow: AuthenticationFlow) {
|
||||
private mutating func setup(authenticationFlow: AuthenticationFlow) {
|
||||
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class SessionDirectoriesTests: XCTestCase {
|
||||
@Suite
|
||||
struct SessionDirectoriesTests {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
func testInitWithDataDirectory() {
|
||||
@Test
|
||||
func initWithDataDirectory() {
|
||||
// Given only a session directory without a caches directory.
|
||||
let sessionDirectoryName = UUID().uuidString
|
||||
let sessionDirectory = URL.applicationSupportBaseDirectory.appending(component: sessionDirectoryName)
|
||||
@@ -21,11 +24,12 @@ class SessionDirectoriesTests: XCTestCase {
|
||||
let sessionDirectories = SessionDirectories(dataDirectory: sessionDirectory)
|
||||
|
||||
// Then the data directory should remain unchanged and the caches directory should be generated.
|
||||
XCTAssertEqual(sessionDirectories.dataDirectory, sessionDirectory)
|
||||
XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName))
|
||||
#expect(sessionDirectories.dataDirectory == sessionDirectory)
|
||||
#expect(sessionDirectories.cacheDirectory == .sessionCachesBaseDirectory.appending(component: sessionDirectoryName))
|
||||
}
|
||||
|
||||
func testPathOutput() {
|
||||
@Test
|
||||
func pathOutput() {
|
||||
// Given session directories created from paths with spaces in them.
|
||||
let originalDataPath = "/Users/John Smith/Data"
|
||||
let originalCachePath = "/Users/John Smith/Caches"
|
||||
@@ -38,53 +42,55 @@ class SessionDirectoriesTests: XCTestCase {
|
||||
let returnedCachePath = sessionDirectories.cachePath
|
||||
|
||||
// Then the paths should not be escaped.
|
||||
XCTAssertEqual(returnedDataPath, originalDataPath)
|
||||
XCTAssertEqual(returnedCachePath, originalCachePath)
|
||||
#expect(returnedDataPath == originalDataPath)
|
||||
#expect(returnedCachePath == originalCachePath)
|
||||
}
|
||||
|
||||
func testDeleteDirectories() throws {
|
||||
@Test
|
||||
func deleteDirectories() throws {
|
||||
// Given a new set of session directories.
|
||||
let sessionDirectories = SessionDirectories()
|
||||
try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true)
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
|
||||
// When deleting the directories.
|
||||
sessionDirectories.delete()
|
||||
|
||||
// Then neither directory should exist on disk.
|
||||
XCTAssertFalse(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
XCTAssertFalse(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
#expect(!fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
#expect(!fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
}
|
||||
|
||||
func testDeleteTransientUserData() throws {
|
||||
@Test
|
||||
func deleteTransientUserData() throws {
|
||||
// Given a set of session directories with some databases.
|
||||
let sessionDirectories = SessionDirectories()
|
||||
try fileManager.createDirectory(at: sessionDirectories.dataDirectory, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: sessionDirectories.cacheDirectory, withIntermediateDirectories: true)
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
|
||||
sessionDirectories.generateMockData()
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
|
||||
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory), 6)
|
||||
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory), 3)
|
||||
#expect(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
|
||||
#expect(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
|
||||
#expect(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
|
||||
#expect(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory) == 6)
|
||||
#expect(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory) == 3)
|
||||
|
||||
// When deleting transient user data.
|
||||
sessionDirectories.deleteTransientUserData()
|
||||
|
||||
// Then the data directory should only contain the crypto store and the cache directory should remain but be empty.
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory), 3)
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.dataDirectory))
|
||||
#expect(try fileManager.numberOfItems(at: sessionDirectories.dataDirectory) == 3)
|
||||
#expect(!fileManager.fileExists(atPath: sessionDirectories.mockStateStorePath))
|
||||
#expect(fileManager.fileExists(atPath: sessionDirectories.mockCryptoStorePath))
|
||||
|
||||
XCTAssertTrue(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
XCTAssertEqual(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory), 0)
|
||||
XCTAssertFalse(fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
|
||||
#expect(fileManager.directoryExists(at: sessionDirectories.cacheDirectory))
|
||||
#expect(try fileManager.numberOfItems(at: sessionDirectories.cacheDirectory) == 0)
|
||||
#expect(!fileManager.fileExists(atPath: sessionDirectories.mockEventCachePath))
|
||||
|
||||
// The tests are done, tidy up these useless directories 🧹
|
||||
sessionDirectories.delete()
|
||||
|
||||
@@ -7,105 +7,108 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SessionVerificationStateMachineTests: XCTestCase {
|
||||
private var stateMachine: SessionVerificationScreenStateMachine!
|
||||
@Suite
|
||||
struct SessionVerificationStateMachineTests {
|
||||
private var stateMachine: SessionVerificationScreenStateMachine
|
||||
|
||||
@MainActor
|
||||
override func setUpWithError() throws {
|
||||
init() {
|
||||
stateMachine = SessionVerificationScreenStateMachine(state: .initial)
|
||||
}
|
||||
|
||||
func testAcceptChallenge() {
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
@Test
|
||||
func acceptChallenge() {
|
||||
#expect(stateMachine.state == .initial)
|
||||
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
XCTAssertEqual(stateMachine.state, .requestingVerification)
|
||||
#expect(stateMachine.state == .requestingVerification)
|
||||
|
||||
stateMachine.processEvent(.didAcceptVerificationRequest)
|
||||
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
|
||||
#expect(stateMachine.state == .verificationRequestAccepted)
|
||||
|
||||
stateMachine.processEvent(.didStartSasVerification)
|
||||
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
|
||||
#expect(stateMachine.state == .sasVerificationStarted)
|
||||
|
||||
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
stateMachine.processEvent(.acceptChallenge)
|
||||
XCTAssertEqual(stateMachine.state, .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(stateMachine.state == .acceptingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
stateMachine.processEvent(.didAcceptChallenge)
|
||||
XCTAssertEqual(stateMachine.state, .verified)
|
||||
#expect(stateMachine.state == .verified)
|
||||
}
|
||||
|
||||
func testDeclineChallenge() {
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
@Test
|
||||
func declineChallenge() {
|
||||
#expect(stateMachine.state == .initial)
|
||||
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
XCTAssertEqual(stateMachine.state, .requestingVerification)
|
||||
#expect(stateMachine.state == .requestingVerification)
|
||||
|
||||
stateMachine.processEvent(.didAcceptVerificationRequest)
|
||||
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
|
||||
#expect(stateMachine.state == .verificationRequestAccepted)
|
||||
|
||||
stateMachine.processEvent(.didStartSasVerification)
|
||||
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
|
||||
#expect(stateMachine.state == .sasVerificationStarted)
|
||||
|
||||
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
stateMachine.processEvent(.declineChallenge)
|
||||
XCTAssertEqual(stateMachine.state, .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(stateMachine.state == .decliningChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
stateMachine.processEvent(.didCancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelled)
|
||||
#expect(stateMachine.state == .cancelled)
|
||||
|
||||
stateMachine.processEvent(.restart)
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
#expect(stateMachine.state == .initial)
|
||||
}
|
||||
|
||||
func testCancellation() {
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
@Test
|
||||
func cancellation() {
|
||||
#expect(stateMachine.state == .initial)
|
||||
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
XCTAssertEqual(stateMachine.state, .requestingVerification)
|
||||
#expect(stateMachine.state == .requestingVerification)
|
||||
|
||||
stateMachine.processEvent(.cancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelling)
|
||||
#expect(stateMachine.state == .cancelling)
|
||||
|
||||
stateMachine.processEvent(.didCancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelled)
|
||||
#expect(stateMachine.state == .cancelled)
|
||||
|
||||
// This duplication is intentional
|
||||
stateMachine.processEvent(.didCancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelled)
|
||||
#expect(stateMachine.state == .cancelled)
|
||||
|
||||
stateMachine.processEvent(.restart)
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
#expect(stateMachine.state == .initial)
|
||||
|
||||
stateMachine.processEvent(.requestVerification)
|
||||
XCTAssertEqual(stateMachine.state, .requestingVerification)
|
||||
#expect(stateMachine.state == .requestingVerification)
|
||||
|
||||
stateMachine.processEvent(.didAcceptVerificationRequest)
|
||||
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
|
||||
#expect(stateMachine.state == .verificationRequestAccepted)
|
||||
|
||||
stateMachine.processEvent(.didStartSasVerification)
|
||||
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
|
||||
#expect(stateMachine.state == .sasVerificationStarted)
|
||||
|
||||
stateMachine.processEvent(.didReceiveChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
#expect(stateMachine.state == .showingChallenge(emojis: SessionVerificationControllerProxyMock.emojis))
|
||||
|
||||
stateMachine.processEvent(.cancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelling)
|
||||
#expect(stateMachine.state == .cancelling)
|
||||
|
||||
stateMachine.processEvent(.didCancel)
|
||||
XCTAssertEqual(stateMachine.state, .cancelled)
|
||||
#expect(stateMachine.state == .cancelled)
|
||||
|
||||
stateMachine.processEvent(.restart)
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
#expect(stateMachine.state == .initial)
|
||||
|
||||
stateMachine.processEvent(.restart)
|
||||
XCTAssertEqual(stateMachine.state, .initial)
|
||||
#expect(stateMachine.state == .initial)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,15 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SettingsScreenViewModelTests: XCTestCase {
|
||||
var viewModel: SettingsScreenViewModelProtocol!
|
||||
var context: SettingsScreenViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
@Suite
|
||||
struct SettingsScreenViewModelTests {
|
||||
private var viewModel: SettingsScreenViewModelProtocol
|
||||
private var context: SettingsScreenViewModelType.Context
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
cancellables.removeAll()
|
||||
init() {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: ""))))
|
||||
viewModel = SettingsScreenViewModel(userSession: userSession,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
@@ -25,19 +24,22 @@ class SettingsScreenViewModelTests: XCTestCase {
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
@MainActor func testLogout() async throws {
|
||||
@Test
|
||||
func logout() async throws {
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .logout }
|
||||
context.send(viewAction: .logout)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testReportBug() async throws {
|
||||
@Test
|
||||
func reportBug() async throws {
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .reportBug }
|
||||
context.send(viewAction: .reportBug)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testAnalytics() async throws {
|
||||
@Test
|
||||
func analytics() async throws {
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .analytics }
|
||||
context.send(viewAction: .analytics)
|
||||
try await deferred.fulfill()
|
||||
|
||||
@@ -7,29 +7,32 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SoftLogoutScreenViewModelTests: XCTestCase {
|
||||
let credentials = SoftLogoutScreenCredentials(userID: "mock_user_id",
|
||||
homeserverName: "https://example.com",
|
||||
userDisplayName: "mock_username",
|
||||
deviceID: "ABCDEFGH")
|
||||
@Suite
|
||||
struct SoftLogoutScreenViewModelTests {
|
||||
private let credentials = SoftLogoutScreenCredentials(userID: "mock_user_id",
|
||||
homeserverName: "https://example.com",
|
||||
userDisplayName: "mock_username",
|
||||
deviceID: "ABCDEFGH")
|
||||
|
||||
func testInitialStateForBasicServer() {
|
||||
@Test
|
||||
func initialStateForBasicServer() {
|
||||
let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
|
||||
homeserver: .mockBasicServer,
|
||||
keyBackupNeeded: false)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The view model should show login form for the given homeserver.")
|
||||
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
#expect(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
#expect(!context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
#expect(context.viewState.loginMode == .password, "The view model should show login form for the given homeserver.")
|
||||
#expect(!context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
}
|
||||
|
||||
func testInitialStateForBasicServerPasswordEntered() {
|
||||
@Test
|
||||
func initialStateForBasicServerPasswordEntered() {
|
||||
let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
|
||||
homeserver: .mockBasicServer,
|
||||
keyBackupNeeded: true,
|
||||
@@ -37,34 +40,36 @@ class SoftLogoutScreenViewModelTests: XCTestCase {
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssertTrue(context.viewState.canSubmit, "The view model should start with a valid password.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .password, "The view model should show login form for the given homeserver.")
|
||||
XCTAssert(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
|
||||
#expect(context.viewState.canSubmit, "The view model should start with a valid password.")
|
||||
#expect(context.viewState.loginMode == .password, "The view model should show login form for the given homeserver.")
|
||||
#expect(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.")
|
||||
}
|
||||
|
||||
func testInitialStateForOIDC() {
|
||||
@Test
|
||||
func initialStateForOIDC() {
|
||||
let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
|
||||
homeserver: .mockMatrixDotOrg,
|
||||
keyBackupNeeded: false)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
XCTAssertTrue(context.viewState.loginMode.supportsOIDCFlow, "The view model should show OIDC button for the given homeserver.")
|
||||
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
#expect(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
#expect(!context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
#expect(context.viewState.loginMode.supportsOIDCFlow, "The view model should show OIDC button for the given homeserver.")
|
||||
#expect(!context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
}
|
||||
|
||||
func testInitialStateForUnsupported() {
|
||||
@Test
|
||||
func initialStateForUnsupported() {
|
||||
let viewModel = SoftLogoutScreenViewModel(credentials: credentials,
|
||||
homeserver: .mockUnsupported,
|
||||
keyBackupNeeded: false)
|
||||
let context = viewModel.context
|
||||
|
||||
// Given a view model where the user hasn't yet sent the verification email.
|
||||
XCTAssert(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
XCTAssertFalse(context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
XCTAssertEqual(context.viewState.loginMode, .unsupported, "The view model should show unsupported text for the given homeserver.")
|
||||
XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
#expect(context.password.isEmpty, "The view model should start with an empty password.")
|
||||
#expect(!context.viewState.canSubmit, "The view model should start with an invalid password.")
|
||||
#expect(context.viewState.loginMode == .unsupported, "The view model should show unsupported text for the given homeserver.")
|
||||
#expect(!context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,35 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SpaceAddRoomsScreenViewModelTests: XCTestCase {
|
||||
var spaceRoomListProxy: SpaceRoomListProxyMock!
|
||||
var spaceServiceProxy: SpaceServiceProxyMock!
|
||||
@Suite
|
||||
struct SpaceAddRoomsScreenViewModelTests {
|
||||
var spaceRoomListProxy: SpaceRoomListProxyMock
|
||||
var spaceServiceProxy: SpaceServiceProxyMock
|
||||
var viewModel: SpaceAddRoomsScreenViewModelProtocol
|
||||
|
||||
var viewModel: SpaceAddRoomsScreenViewModelProtocol!
|
||||
var context: SpaceAddRoomsScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
func testAddingChildRoom() async throws {
|
||||
setupViewModel()
|
||||
init() {
|
||||
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(isSpace: true)))
|
||||
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.recentlyVisitedRoomsFilterReturnValue = .init(repeating: JoinedRoomProxyMock(.init()), count: 5)
|
||||
spaceServiceProxy = clientProxy.underlyingSpaceService as? SpaceServiceProxyMock ?? SpaceServiceProxyMock(.init())
|
||||
|
||||
viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
roomSummaryProvider: summaryProvider,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
@Test
|
||||
func addingChildRoom() async throws {
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
|
||||
message: "The screen should start with some suggestions.") { section in
|
||||
section.type == .suggestions && !section.rooms.isEmpty
|
||||
@@ -37,23 +51,22 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
|
||||
context.send(viewAction: .searchQueryChanged)
|
||||
try await deferred.fulfill()
|
||||
|
||||
let room = try XCTUnwrap(context.viewState.roomsSection.rooms.first)
|
||||
let room = try #require(context.viewState.roomsSection.rooms.first, "Expected a room in the section")
|
||||
context.send(viewAction: .toggleRoom(room))
|
||||
XCTAssertTrue(context.viewState.selectedRooms.contains(room), "The selected room should be shown.")
|
||||
#expect(context.viewState.selectedRooms.contains(room), "The selected room should be shown.")
|
||||
|
||||
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||
context.send(viewAction: .save)
|
||||
|
||||
try await deferredAction.fulfill()
|
||||
|
||||
XCTAssertTrue(spaceServiceProxy.addChildToCalled, "The room should have been added to the space.")
|
||||
XCTAssertTrue(spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
#expect(spaceServiceProxy.addChildToCalled, "The room should have been added to the space.")
|
||||
#expect(spaceRoomListProxy.resetCalled, "The room list should be reset to pick up the changes.")
|
||||
}
|
||||
|
||||
func testFailureWithMultipleRoomsSelected() async throws {
|
||||
@Test
|
||||
func failureWithMultipleRoomsSelected() async throws {
|
||||
// Given a view model with 4 selected rooms.
|
||||
setupViewModel()
|
||||
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.roomsSection),
|
||||
message: "There should be 4 search results.") { section in
|
||||
section.type == .searchResults && section.rooms.count == 4
|
||||
@@ -65,7 +78,7 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
|
||||
for room in context.viewState.roomsSection.rooms {
|
||||
context.send(viewAction: .toggleRoom(room))
|
||||
}
|
||||
XCTAssertEqual(context.viewState.selectedRooms.count, 4, "All of the rooms should be selected.")
|
||||
#expect(context.viewState.selectedRooms.count == 4, "All of the rooms should be selected.")
|
||||
|
||||
// When there's a failure half way through saving.
|
||||
let successfulIDs = context.viewState.roomsSection.rooms.map(\.id).prefix(2)
|
||||
@@ -85,24 +98,10 @@ class SpaceAddRoomsScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the screen should be updated to only show the rooms that still need to be added.
|
||||
XCTAssertEqual(spaceServiceProxy.addChildToCallsCount, 3, "The remaining calls to the service should stop after a failure.")
|
||||
XCTAssertFalse(context.viewState.selectedRooms.contains { successfulIDs.contains($0.id) },
|
||||
"The added rooms should no longer show as selected.")
|
||||
XCTAssertFalse(context.viewState.roomsSection.rooms.contains { successfulIDs.contains($0.id) },
|
||||
"The added rooms should no longer be listed for selection.")
|
||||
}
|
||||
|
||||
func setupViewModel() {
|
||||
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: SpaceServiceRoom.mock(isSpace: true)))
|
||||
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
clientProxy.recentlyVisitedRoomsFilterReturnValue = .init(repeating: JoinedRoomProxyMock(.init()), count: 5)
|
||||
spaceServiceProxy = clientProxy.underlyingSpaceService as? SpaceServiceProxyMock
|
||||
|
||||
viewModel = SpaceAddRoomsScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||
userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
roomSummaryProvider: summaryProvider,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
#expect(spaceServiceProxy.addChildToCallsCount == 3, "The remaining calls to the service should stop after a failure.")
|
||||
#expect(!context.viewState.selectedRooms.contains { successfulIDs.contains($0.id) },
|
||||
"The added rooms should no longer show as selected.")
|
||||
#expect(!context.viewState.roomsSection.rooms.contains { successfulIDs.contains($0.id) },
|
||||
"The added rooms should no longer be listed for selection.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,90 +8,24 @@
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class SpacesScreenViewModelTests: XCTestCase {
|
||||
var topLevelSpacesSubject: CurrentValueSubject<[SpaceServiceRoom], Never>!
|
||||
var spaceServiceProxy: SpaceServiceProxyMock!
|
||||
var appSettings: AppSettings!
|
||||
|
||||
var viewModel: SpacesScreenViewModelProtocol!
|
||||
@Suite
|
||||
final class SpacesScreenViewModelTests {
|
||||
var topLevelSpacesSubject: CurrentValueSubject<[SpaceServiceRoom], Never>
|
||||
var spaceServiceProxy: SpaceServiceProxyMock
|
||||
var appSettings: AppSettings
|
||||
var viewModel: SpacesScreenViewModelProtocol
|
||||
|
||||
var context: SpacesScreenViewModelType.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
setupViewModel()
|
||||
XCTAssertEqual(context.viewState.topLevelSpaces.count, 3)
|
||||
}
|
||||
|
||||
func testTopLevelSpacesSubscription() async throws {
|
||||
setupViewModel()
|
||||
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 0 }
|
||||
topLevelSpacesSubject.send([])
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.topLevelSpaces.count, 0)
|
||||
|
||||
deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 1 }
|
||||
topLevelSpacesSubject.send([
|
||||
SpaceServiceRoom.mock(isSpace: true)
|
||||
])
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.topLevelSpaces.count, 1)
|
||||
}
|
||||
|
||||
func testSelectingSpace() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let selectedSpace = topLevelSpacesSubject.value[0]
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
|
||||
viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace)))
|
||||
let action = try await deferred.fulfill()
|
||||
|
||||
switch action {
|
||||
case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.id == selectedSpace.id:
|
||||
break
|
||||
default:
|
||||
XCTFail("The action should select the space.")
|
||||
}
|
||||
}
|
||||
|
||||
func testFeatureAnnouncement() async throws {
|
||||
setupViewModel()
|
||||
XCTAssertFalse(appSettings.hasSeenSpacesAnnouncement)
|
||||
XCTAssertFalse(context.isPresentingFeatureAnnouncement)
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.isPresentingFeatureAnnouncement)) { $0 == true }
|
||||
viewModel.context.send(viewAction: .screenAppeared)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertTrue(context.isPresentingFeatureAnnouncement)
|
||||
|
||||
viewModel.context.send(viewAction: .featureAnnouncementAppeared)
|
||||
XCTAssertTrue(appSettings.hasSeenSpacesAnnouncement)
|
||||
|
||||
context.isPresentingFeatureAnnouncement = false
|
||||
|
||||
let deferredFailure = deferFailure(context.observe(\.isPresentingFeatureAnnouncement), timeout: 1) { $0 == true }
|
||||
viewModel.context.send(viewAction: .screenAppeared)
|
||||
try await deferredFailure.fulfill()
|
||||
XCTAssertFalse(context.isPresentingFeatureAnnouncement)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel() {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
@@ -103,7 +37,7 @@ class SpacesScreenViewModelTests: XCTestCase {
|
||||
spaceServiceProxy = SpaceServiceProxyMock(.init())
|
||||
spaceServiceProxy.topLevelSpacesPublisher = topLevelSpacesSubject.asCurrentValuePublisher()
|
||||
spaceServiceProxy.spaceRoomListSpaceIDClosure = { [topLevelSpacesSubject] spaceID in
|
||||
guard let spaceServiceRoom = topLevelSpacesSubject?.value.first(where: { $0.id == spaceID }) else { return .failure(.missingSpace) }
|
||||
guard let spaceServiceRoom = topLevelSpacesSubject.value.first(where: { $0.id == spaceID }) else { return .failure(.missingSpace) }
|
||||
return .success(SpaceRoomListProxyMock(.init(spaceServiceRoom: spaceServiceRoom)))
|
||||
}
|
||||
clientProxy.spaceService = spaceServiceProxy
|
||||
@@ -113,4 +47,64 @@ class SpacesScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
@Test
|
||||
func initialState() {
|
||||
#expect(context.viewState.topLevelSpaces.count == 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
func topLevelSpacesSubscription() async throws {
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 0 }
|
||||
topLevelSpacesSubject.send([])
|
||||
try await deferred.fulfill()
|
||||
#expect(context.viewState.topLevelSpaces.count == 0)
|
||||
|
||||
deferred = deferFulfillment(context.observe(\.viewState.topLevelSpaces)) { $0.count == 1 }
|
||||
topLevelSpacesSubject.send([
|
||||
SpaceServiceRoom.mock(isSpace: true)
|
||||
])
|
||||
try await deferred.fulfill()
|
||||
#expect(context.viewState.topLevelSpaces.count == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func selectingSpace() async throws {
|
||||
let selectedSpace = topLevelSpacesSubject.value[0]
|
||||
let deferred = deferFulfillment(viewModel.actionsPublisher) { _ in true }
|
||||
viewModel.context.send(viewAction: .spaceAction(.select(selectedSpace)))
|
||||
let action = try await deferred.fulfill()
|
||||
|
||||
switch action {
|
||||
case .selectSpace(let spaceRoomListProxy) where spaceRoomListProxy.id == selectedSpace.id:
|
||||
break
|
||||
default:
|
||||
Issue.record("The action should select the space.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func featureAnnouncement() async throws {
|
||||
#expect(!appSettings.hasSeenSpacesAnnouncement)
|
||||
#expect(!context.isPresentingFeatureAnnouncement)
|
||||
|
||||
let deferred = deferFulfillment(context.observe(\.isPresentingFeatureAnnouncement)) { $0 == true }
|
||||
viewModel.context.send(viewAction: .screenAppeared)
|
||||
try await deferred.fulfill()
|
||||
#expect(context.isPresentingFeatureAnnouncement)
|
||||
|
||||
viewModel.context.send(viewAction: .featureAnnouncementAppeared)
|
||||
#expect(appSettings.hasSeenSpacesAnnouncement)
|
||||
|
||||
context.isPresentingFeatureAnnouncement = false
|
||||
|
||||
let deferredFailure = deferFailure(context.observe(\.isPresentingFeatureAnnouncement), timeout: .seconds(1)) { $0 == true }
|
||||
viewModel.context.send(viewAction: .screenAppeared)
|
||||
try await deferredFailure.fulfill()
|
||||
#expect(!context.isPresentingFeatureAnnouncement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,20 @@
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
class StartChatScreenViewModelTests: XCTestCase {
|
||||
var viewModel: StartChatScreenViewModelProtocol!
|
||||
var clientProxy: ClientProxyMock!
|
||||
var userDiscoveryService: UserDiscoveryServiceMock!
|
||||
@Suite
|
||||
struct StartChatScreenViewModelTests {
|
||||
private var viewModel: StartChatScreenViewModelProtocol!
|
||||
private var clientProxy: ClientProxyMock!
|
||||
private var userDiscoveryService: UserDiscoveryServiceMock!
|
||||
|
||||
var context: StartChatScreenViewModel.Context {
|
||||
private var context: StartChatScreenViewModel.Context {
|
||||
viewModel.context
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
init() {
|
||||
clientProxy = .init(.init(userID: ""))
|
||||
userDiscoveryService = UserDiscoveryServiceMock()
|
||||
userDiscoveryService.searchProfilesWithReturnValue = .success([])
|
||||
@@ -31,21 +32,23 @@ class StartChatScreenViewModelTests: XCTestCase {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
|
||||
func testQueryShowingNoResults() async {
|
||||
@Test
|
||||
mutating func queryShowingNoResults() async {
|
||||
await search(query: "A")
|
||||
XCTAssertEqual(context.viewState.usersSection.type, .suggestions)
|
||||
#expect(context.viewState.usersSection.type == .suggestions)
|
||||
|
||||
await search(query: "AA")
|
||||
XCTAssertEqual(context.viewState.usersSection.type, .suggestions)
|
||||
XCTAssertFalse(userDiscoveryService.searchProfilesWithCalled)
|
||||
#expect(context.viewState.usersSection.type == .suggestions)
|
||||
#expect(!userDiscoveryService.searchProfilesWithCalled)
|
||||
|
||||
await search(query: "AAA")
|
||||
assertSearchResults(toBe: 0)
|
||||
|
||||
XCTAssertTrue(userDiscoveryService.searchProfilesWithCalled)
|
||||
#expect(userDiscoveryService.searchProfilesWithCalled)
|
||||
}
|
||||
|
||||
func testJoinRoomByAddress() async throws {
|
||||
@Test
|
||||
func joinRoomByAddress() async throws {
|
||||
clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "id", servers: []))
|
||||
|
||||
let deferredViewState = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -61,7 +64,8 @@ class StartChatScreenViewModelTests: XCTestCase {
|
||||
try await deferredAction.fulfill()
|
||||
}
|
||||
|
||||
func testJoinRoomByAddressFailsBecauseInvalid() async throws {
|
||||
@Test
|
||||
func joinRoomByAddressFailsBecauseInvalid() async throws {
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
viewState.joinByAddressState == .invalidAddress
|
||||
}
|
||||
@@ -70,7 +74,8 @@ class StartChatScreenViewModelTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testJoinRoomByAddressFailsBecauseNotFound() async throws {
|
||||
@Test
|
||||
func joinRoomByAddressFailsBecauseNotFound() async throws {
|
||||
clientProxy.resolveRoomAliasReturnValue = .failure(.failedResolvingRoomAlias)
|
||||
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
|
||||
@@ -84,14 +89,14 @@ class StartChatScreenViewModelTests: XCTestCase {
|
||||
// MARK: - Private
|
||||
|
||||
private func assertSearchResults(toBe count: Int) {
|
||||
XCTAssertTrue(count >= 0)
|
||||
XCTAssertEqual(context.viewState.usersSection.type, .searchResult)
|
||||
XCTAssertEqual(context.viewState.usersSection.users.count, count)
|
||||
XCTAssertEqual(context.viewState.hasEmptySearchResults, count == 0)
|
||||
#expect(count >= 0)
|
||||
#expect(context.viewState.usersSection.type == .searchResult)
|
||||
#expect(context.viewState.usersSection.users.count == count)
|
||||
#expect(context.viewState.hasEmptySearchResults == (count == 0))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func search(query: String) async -> StartChatScreenViewState? {
|
||||
private mutating func search(query: String) async -> StartChatScreenViewState? {
|
||||
viewModel.context.searchQuery = query
|
||||
return await context.$viewState.nextValue
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user