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:
Mauro
2026-02-19 16:20:47 +01:00
committed by GitHub
parent c92e847ed7
commit 173b39a07f
118 changed files with 4630 additions and 4129 deletions

View File

@@ -11,6 +11,7 @@
--commas inline
--ifdef no-indent
--indent 4
--nospaceoperators ...,..<
--stripunusedargs closure-only
--trimwhitespace nonblank-lines

View File

@@ -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 */,

View 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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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.")
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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") == "")
}
}

View File

@@ -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))
}
}

View File

@@ -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]])
}
}

View File

@@ -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) {

View File

@@ -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.")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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.")
}
}

View File

@@ -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
}

View File

@@ -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 { }

View File

@@ -24,7 +24,7 @@ class ChatsTabFlowCoordinatorTests: XCTestCase {
var detailCoordinator: CoordinatorProtocol? {
splitCoordinator?.detailCoordinator
}
var detailNavigationStack: NavigationStackCoordinator? {
detailCoordinator as? NavigationStackCoordinator
}

View File

@@ -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 == []
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 { }

View File

@@ -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() }

View File

@@ -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))
}
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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()),

View File

@@ -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.")

View File

@@ -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.")
}
}

View File

@@ -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 &&

View File

@@ -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 { }

View File

@@ -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")
}
}

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 == [])
}
}

View File

@@ -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"]))
}
}

View File

@@ -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")
}
}

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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.")
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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.")
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 { }

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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()),

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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(),

View File

@@ -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))
}
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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 { }

View File

@@ -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)

View File

@@ -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 { }

View File

@@ -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 { }

View File

@@ -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.")
}
}

View File

@@ -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(),

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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.")
}
}

View File

@@ -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.")
}
}

View File

@@ -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)
}
}

View File

@@ -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