diff --git a/.swiftformat b/.swiftformat index f9f86ca8e..0c6722736 100644 --- a/.swiftformat +++ b/.swiftformat @@ -11,6 +11,7 @@ --commas inline --ifdef no-indent +--indent 4 --nospaceoperators ...,..< --stripunusedargs closure-only --trimwhitespace nonblank-lines diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 197812d83..fe8d98ca9 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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 = ""; }; 2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = ""; }; - 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = ""; }; 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = ""; }; @@ -1906,7 +1898,6 @@ 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = ""; }; 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = ""; }; - 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = ""; }; 4048547AC50ADCF201684E87 /* EditRoomAddressScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreen.swift; sourceTree = ""; }; 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = ""; }; 407C8DD85179D2DB896FC0FA /* RoomFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorStateMachine.swift; sourceTree = ""; }; @@ -2140,10 +2131,8 @@ 6C9651CD1066F239C7739240 /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsScreenIdentifier.swift; sourceTree = ""; }; - 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelTests.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatViewModelTests.swift; sourceTree = ""; }; - 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = ""; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; @@ -2611,7 +2600,6 @@ C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = ""; }; C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; - C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = ""; }; C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = ""; }; C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; @@ -2627,6 +2615,7 @@ C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = ""; }; C33B3F17996DFDF5F0181512 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = ""; }; + C39E32F0B876B962E418B5C2 /* DeferredFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredFulfillment.swift; sourceTree = ""; }; C4756240773D26AB74C22668 /* OrientationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationManagerProtocol.swift; sourceTree = ""; }; C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenViewModelProtocol.swift; sourceTree = ""; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2665,7 +2654,6 @@ CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = ""; }; CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = ""; }; CB98BFD8E93C7FCCEDEC46F9 /* SpacesScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesScreenViewModel.swift; sourceTree = ""; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; @@ -2853,7 +2841,6 @@ ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; - EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = ""; }; EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = ""; }; EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = ""; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = ""; }; @@ -2883,7 +2870,6 @@ F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F320003F490B11F808ECC5E9 /* JoinedMembersBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedMembersBadgeView.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; - F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenCoordinator.swift; sourceTree = ""; }; F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; F3AAC314A877DBDB6EBE1170 /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = ""; }; @@ -4366,14 +4352,6 @@ path = Scripts; sourceTree = ""; }; - 53280D2292E6C9C7821773FD /* UserSession */ = { - isa = PBXGroup; - children = ( - F36C0A6D59717193F49EA986 /* UserSessionTests.swift */, - ); - path = UserSession; - sourceTree = ""; - }; 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 */, diff --git a/ElementX/Sources/Other/DeferredFulfillment.swift b/ElementX/Sources/Other/DeferredFulfillment.swift new file mode 100644 index 000000000..ec7f4e7d9 --- /dev/null +++ b/ElementX/Sources/Other/DeferredFulfillment.swift @@ -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 { + 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(_ publisher: P, + timeout: Duration = .seconds(10), + message: String? = nil, + until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment { + var result: Result? + 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 { + 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(_ asyncSequence: any AsyncSequence, + timeout: Duration = .seconds(10), + message: String? = nil, + until condition: @escaping (Value) -> Bool) -> DeferredFulfillment { + var result: Result? + var hasFulfilled = false + + let task = Task { + for await value in asyncSequence { + if condition(value), !hasFulfilled { + result = .success(value) + hasFulfilled = true + } + } + } + + return DeferredFulfillment { + 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, V: Equatable>(_ publisher: P, + keyPath: K, + transitionValues: [V], + timeout: Duration = .seconds(10)) -> DeferredFulfillment { + 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(_ asyncSequence: any AsyncSequence, + transitionValues: [Value], + timeout: Duration = .seconds(10)) -> DeferredFulfillment { + 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(_ publisher: P, + timeout: Duration, + message: String? = nil, + until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment where P.Failure == Never { + var hasFulfilled = false + let cancellable = publisher + .sink { value in + if condition(value), !hasFulfilled { + hasFulfilled = true + } + } + + return DeferredFulfillment { + 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(_ asyncSequence: any AsyncSequence, + timeout: Duration, + message: String? = nil, + until condition: @escaping (Value) -> Bool) -> DeferredFulfillment { + var hasFulfilled = false + + let task = Task { + for await value in asyncSequence { + if condition(value), !hasFulfilled { + hasFulfilled = true + } + } + } + + return DeferredFulfillment { + 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) + } + } +} diff --git a/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift b/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift index bef447007..69a261d10 100644 --- a/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift +++ b/UnitTests/Sources/AVMetadataMachineReadableCodeObjectExtensionsTest.swift @@ -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) } } diff --git a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift index 61c5491f0..9c3cc662c 100644 --- a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift index 2a0b9174a..92c477bf4 100644 --- a/UnitTests/Sources/AnalyticsTests.swift +++ b/UnitTests/Sources/AnalyticsTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift index 28c75bc97..92fcc9f05 100644 --- a/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockScreenViewModelTests.swift @@ -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") } } diff --git a/UnitTests/Sources/AppLock/AppLockServiceTests.swift b/UnitTests/Sources/AppLock/AppLockServiceTests.swift index c2cd81905..835590496 100644 --- a/UnitTests/Sources/AppLock/AppLockServiceTests.swift +++ b/UnitTests/Sources/AppLock/AppLockServiceTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift index ef02fd2e4..65366c359 100644 --- a/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift index adc125b2f..8900c371b 100644 --- a/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift index 172318b0a..f681bc46e 100644 --- a/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AppLock/AppLockTimerTests.swift b/UnitTests/Sources/AppLock/AppLockTimerTests.swift index ac2eef209..675b915dc 100644 --- a/UnitTests/Sources/AppLock/AppLockTimerTests.swift +++ b/UnitTests/Sources/AppLock/AppLockTimerTests.swift @@ -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 } } diff --git a/UnitTests/Sources/AppLock/PINTextFieldTests.swift b/UnitTests/Sources/AppLock/PINTextFieldTests.swift index 0cd55cee0..9c9393229 100644 --- a/UnitTests/Sources/AppLock/PINTextFieldTests.swift +++ b/UnitTests/Sources/AppLock/PINTextFieldTests.swift @@ -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") == "") } } diff --git a/UnitTests/Sources/AppRouteURLParserTests.swift b/UnitTests/Sources/AppRouteURLParserTests.swift index 0a07c2abf..03762b868 100644 --- a/UnitTests/Sources/AppRouteURLParserTests.swift +++ b/UnitTests/Sources/AppRouteURLParserTests.swift @@ -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)) } } diff --git a/UnitTests/Sources/ArrayTests.swift b/UnitTests/Sources/ArrayTests.swift index 561a2b1b1..ec02d6076 100644 --- a/UnitTests/Sources/ArrayTests.swift +++ b/UnitTests/Sources/ArrayTests.swift @@ -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]]) } } diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 185004d56..5d583870a 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -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 = "Test string " @@ -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. But this is hidden and this link too" @@ -1138,7 +1138,7 @@ class AttributedStringBuilderTests: XCTestCase { return } } - + // MARK: - Private private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) { diff --git a/UnitTests/Sources/AttributedStringTests.swift b/UnitTests/Sources/AttributedStringTests.swift index ad8b821be..d6fa9765e 100644 --- a/UnitTests/Sources/AttributedStringTests.swift +++ b/UnitTests/Sources/AttributedStringTests.swift @@ -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 \(boldString) Normal.") else { - XCTFail("The attributed string should be built from the HTML.") - return - } + let originalString = try #require(AttributedStringBuilder(mentionBuilder: MentionBuilder()) + .fromHTML("Normal \(boldString) 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.") } } } diff --git a/UnitTests/Sources/AudioPlayerStateTests.swift b/UnitTests/Sources/AudioPlayerStateTests.swift index d56b8ecfb..fdd41dac5 100644 --- a/UnitTests/Sources/AudioPlayerStateTests.swift +++ b/UnitTests/Sources/AudioPlayerStateTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AudioRecorderStateTests.swift b/UnitTests/Sources/AudioRecorderStateTests.swift index d4584f69d..5d321d15b 100644 --- a/UnitTests/Sources/AudioRecorderStateTests.swift +++ b/UnitTests/Sources/AudioRecorderStateTests.swift @@ -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 { 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) } } diff --git a/UnitTests/Sources/AudioRecorderTests.swift b/UnitTests/Sources/AudioRecorderTests.swift index a8e61cf74..ad5e42051 100644 --- a/UnitTests/Sources/AudioRecorderTests.swift +++ b/UnitTests/Sources/AudioRecorderTests.swift @@ -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) } } diff --git a/UnitTests/Sources/AuthenticationServiceTests.swift b/UnitTests/Sources/AuthenticationServiceTests.swift index ca509cf02..9709fdc6c 100644 --- a/UnitTests/Sources/AuthenticationServiceTests.swift +++ b/UnitTests/Sources/AuthenticationServiceTests.swift @@ -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) diff --git a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift index c65333953..9a8daf5af 100644 --- a/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift +++ b/UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift @@ -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 diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift index fca008160..fb5fdf336 100644 --- a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/BugReportScreenViewModelTests.swift b/UnitTests/Sources/BugReportScreenViewModelTests.swift index 82c705ffb..2c876c65c 100644 --- a/UnitTests/Sources/BugReportScreenViewModelTests.swift +++ b/UnitTests/Sources/BugReportScreenViewModelTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/BugReportServiceTests.swift b/UnitTests/Sources/BugReportServiceTests.swift index 19131f141..59fa2cd63 100644 --- a/UnitTests/Sources/BugReportServiceTests.swift +++ b/UnitTests/Sources/BugReportServiceTests.swift @@ -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(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 = .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 = .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 = .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(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(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 } diff --git a/UnitTests/Sources/CallScreenViewModelTests.swift b/UnitTests/Sources/CallScreenViewModelTests.swift deleted file mode 100644 index 107b6494b..000000000 --- a/UnitTests/Sources/CallScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/ChatsTabFlowCoordinatorTests.swift b/UnitTests/Sources/ChatsTabFlowCoordinatorTests.swift index fcdb5ee2a..5772b8dc9 100644 --- a/UnitTests/Sources/ChatsTabFlowCoordinatorTests.swift +++ b/UnitTests/Sources/ChatsTabFlowCoordinatorTests.swift @@ -24,7 +24,7 @@ class ChatsTabFlowCoordinatorTests: XCTestCase { var detailCoordinator: CoordinatorProtocol? { splitCoordinator?.detailCoordinator } - + var detailNavigationStack: NavigationStackCoordinator? { detailCoordinator as? NavigationStackCoordinator } diff --git a/UnitTests/Sources/CompletionSuggestionServiceTests.swift b/UnitTests/Sources/CompletionSuggestionServiceTests.swift index 65dd10e63..38bb7195b 100644 --- a/UnitTests/Sources/CompletionSuggestionServiceTests.swift +++ b/UnitTests/Sources/CompletionSuggestionServiceTests.swift @@ -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 == [] } diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 5a28c49e0..eb8288399 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -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, "#room-alias:matrix.org ") } @@ -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), diff --git a/UnitTests/Sources/DateTests.swift b/UnitTests/Sources/DateTests.swift index 4cf55d172..0eaa2ba3c 100644 --- a/UnitTests/Sources/DateTests.swift +++ b/UnitTests/Sources/DateTests.swift @@ -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 diff --git a/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift b/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift index c3a181b69..1de45d587 100644 --- a/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift +++ b/UnitTests/Sources/DeactivateAccountScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/DeclineAndBlockScreenViewModelTests.swift b/UnitTests/Sources/DeclineAndBlockScreenViewModelTests.swift index 3dcddf499..4a7b11529 100644 --- a/UnitTests/Sources/DeclineAndBlockScreenViewModelTests.swift +++ b/UnitTests/Sources/DeclineAndBlockScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/DeferredFulfillmentTests.swift b/UnitTests/Sources/DeferredFulfillmentTests.swift index ee9d3233a..1b2463048 100644 --- a/UnitTests/Sources/DeferredFulfillmentTests.swift +++ b/UnitTests/Sources/DeferredFulfillmentTests.swift @@ -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) } } diff --git a/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift b/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift deleted file mode 100644 index 4c74939f8..000000000 --- a/UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift b/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift index de46e35b5..a68469356 100644 --- a/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift +++ b/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift @@ -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() } diff --git a/UnitTests/Sources/ElementCallServiceTests.swift b/UnitTests/Sources/ElementCallServiceTests.swift index 39e902e07..702df9ebb 100644 --- a/UnitTests/Sources/ElementCallServiceTests.swift +++ b/UnitTests/Sources/ElementCallServiceTests.swift @@ -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! - var pushRegistry: PKPushRegistry! +@Suite +final class ElementCallServiceTests { + private var callProvider: CXProviderMock! + private var currentDate: Date! + private var testClock: TestClock! + 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)) } } diff --git a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift index ce53add0e..d316c04d5 100644 --- a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift +++ b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift @@ -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 = []) { + private mutating func setupViewModel(selectedEmojis: Set = []) { timelineProxy = TimelineProxyMock(.init()) viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent, diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift index 08e6d4884..69906fd7f 100644 --- a/UnitTests/Sources/EmojiProviderTests.swift +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -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) } } diff --git a/UnitTests/Sources/ExpiringTaskRunnerTests.swift b/UnitTests/Sources/ExpiringTaskRunnerTests.swift index 8000c2b6b..e0d9bb513 100644 --- a/UnitTests/Sources/ExpiringTaskRunnerTests.swift +++ b/UnitTests/Sources/ExpiringTaskRunnerTests.swift @@ -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> = 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) } } } diff --git a/UnitTests/Sources/GeoURITests.swift b/UnitTests/Sources/GeoURITests.swift index 24e7b3154..9f3dc9ee4 100644 --- a/UnitTests/Sources/GeoURITests.swift +++ b/UnitTests/Sources/GeoURITests.swift @@ -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) } } diff --git a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift b/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift index c9871f466..8b55fa173 100644 --- a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift +++ b/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift @@ -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() - 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() } } diff --git a/UnitTests/Sources/HomeScreenRoomTests.swift b/UnitTests/Sources/HomeScreenRoomTests.swift index 80a7459b6..102b39d92 100644 --- a/UnitTests/Sources/HomeScreenRoomTests.swift +++ b/UnitTests/Sources/HomeScreenRoomTests.swift @@ -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) } } diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 4e3636f0d..da19e0af4 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -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() - 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(.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(.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(.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? = nil, invites: InviteType? = nil) { + cancellables.removeAll() + var rooms: [RoomSummary] = .mockRooms switch invites { diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index 1551fe511..e94708a1e 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -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()), diff --git a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift index c245fdbbc..c856683bd 100644 --- a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift @@ -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.") diff --git a/UnitTests/Sources/KeychainControllerTests.swift b/UnitTests/Sources/KeychainControllerTests.swift index 037877d23..231d1a99f 100644 --- a/UnitTests/Sources/KeychainControllerTests.swift +++ b/UnitTests/Sources/KeychainControllerTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift b/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift index 29eaa7099..e5a8ca44e 100644 --- a/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift +++ b/UnitTests/Sources/KnockRequestsListScreenViewModelTests.swift @@ -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 && diff --git a/UnitTests/Sources/LegalInformationScreenViewModelTests.swift b/UnitTests/Sources/LegalInformationScreenViewModelTests.swift deleted file mode 100644 index a6cff4ebe..000000000 --- a/UnitTests/Sources/LegalInformationScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/LocalizationTests.swift b/UnitTests/Sources/LocalizationTests.swift index ad41bc0bf..067e10d7b 100644 --- a/UnitTests/Sources/LocalizationTests.swift +++ b/UnitTests/Sources/LocalizationTests.swift @@ -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") } } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 83e81a438..1f5782838 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -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), diff --git a/UnitTests/Sources/LoginScreenViewModelTests.swift b/UnitTests/Sources/LoginScreenViewModelTests.swift index 22adcc817..9e8214f4b 100644 --- a/UnitTests/Sources/LoginScreenViewModelTests.swift +++ b/UnitTests/Sources/LoginScreenViewModelTests.swift @@ -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 } diff --git a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift index 750863753..8bd6cdb33 100644 --- a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift +++ b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/MapTilerURLBuilderTests.swift b/UnitTests/Sources/MapTilerURLBuilderTests.swift index 44664e6d2..d4590847e 100644 --- a/UnitTests/Sources/MapTilerURLBuilderTests.swift +++ b/UnitTests/Sources/MapTilerURLBuilderTests.swift @@ -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) } } diff --git a/UnitTests/Sources/MatrixEntityRegexTests.swift b/UnitTests/Sources/MatrixEntityRegexTests.swift index 46ffa6ea4..1201d7d5c 100644 --- a/UnitTests/Sources/MatrixEntityRegexTests.swift +++ b/UnitTests/Sources/MatrixEntityRegexTests.swift @@ -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")) } } diff --git a/UnitTests/Sources/MediaPlayerProviderTests.swift b/UnitTests/Sources/MediaPlayerProviderTests.swift index 00386c5e1..b47d21775 100644 --- a/UnitTests/Sources/MediaPlayerProviderTests.swift +++ b/UnitTests/Sources/MediaPlayerProviderTests.swift @@ -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().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().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) } } } diff --git a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift index 5f3a04ffa..dde25d63c 100644 --- a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift @@ -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) } } diff --git a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift index 3ce960551..0d7177262 100644 --- a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift +++ b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift @@ -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 { diff --git a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift index caad50cb0..5cc9c8da6 100644 --- a/UnitTests/Sources/MediaUploadingPreprocessorTests.swift +++ b/UnitTests/Sources/MediaUploadingPreprocessorTests.swift @@ -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 diff --git a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift index 5a01cb130..1f0a9f970 100644 --- a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift +++ b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift @@ -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() - 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() } } diff --git a/UnitTests/Sources/NavigationRootCoordinatorTests.swift b/UnitTests/Sources/NavigationRootCoordinatorTests.swift index 1e7c50e3f..69641f54e 100644 --- a/UnitTests/Sources/NavigationRootCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationRootCoordinatorTests.swift @@ -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) } } diff --git a/UnitTests/Sources/NavigationStackCoordinatorTests.swift b/UnitTests/Sources/NavigationStackCoordinatorTests.swift index 505df3926..4eaf3e15a 100644 --- a/UnitTests/Sources/NavigationStackCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationStackCoordinatorTests.swift @@ -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) } } diff --git a/UnitTests/Sources/NavigationTabCoordinatorTests.swift b/UnitTests/Sources/NavigationTabCoordinatorTests.swift index 9b947fd2a..5623deaf2 100644 --- a/UnitTests/Sources/NavigationTabCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationTabCoordinatorTests.swift @@ -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! + private var navigationTabCoordinator: NavigationTabCoordinator - 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 } diff --git a/UnitTests/Sources/NotificationContentBuilderTests.swift b/UnitTests/Sources/NotificationContentBuilderTests.swift index 02ccdd26b..c861591b5 100644 --- a/UnitTests/Sources/NotificationContentBuilderTests.swift +++ b/UnitTests/Sources/NotificationContentBuilderTests.swift @@ -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 == []) } } diff --git a/UnitTests/Sources/PermalinkTests.swift b/UnitTests/Sources/PermalinkTests.swift index 0d3ac2118..c85e974e3 100644 --- a/UnitTests/Sources/PermalinkTests.swift +++ b/UnitTests/Sources/PermalinkTests.swift @@ -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"])) } } diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index f9612424a..b8fc8bdbb 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -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") } } diff --git a/UnitTests/Sources/PinnedEventsBannerStateTests.swift b/UnitTests/Sources/PinnedEventsBannerStateTests.swift index 2baa96fbe..b41174d8c 100644 --- a/UnitTests/Sources/PinnedEventsBannerStateTests.swift +++ b/UnitTests/Sources/PinnedEventsBannerStateTests.swift @@ -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) } } diff --git a/UnitTests/Sources/PollFormScreenViewModelTests.swift b/UnitTests/Sources/PollFormScreenViewModelTests.swift index 63f27ec3c..ded37d477 100644 --- a/UnitTests/Sources/PollFormScreenViewModelTests.swift +++ b/UnitTests/Sources/PollFormScreenViewModelTests.swift @@ -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, diff --git a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift index de188df50..9d1759660 100644 --- a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift +++ b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift @@ -9,89 +9,97 @@ import Combine @testable import ElementX import MatrixRustSDKMocks -import XCTest +import Testing @MainActor -final class QRCodeLoginScreenViewModelTests: XCTestCase { - private var qrLoginProgressSubject: CurrentValueSubject! - private var qrCodeLoginService: QRCodeLoginServiceMock! +@Suite +struct QRCodeLoginScreenViewModelTests { + private enum Mode { case login, linkDesktop, linkMobile } - private var linkMobileProgressSubject: CurrentValueSubject! - private var linkDesktopProgressSubject: CurrentValueSubject! - private var linkNewDeviceService: LinkNewDeviceServiceMock! + var qrLoginProgressSubject: CurrentValueSubject! + var qrCodeLoginService: QRCodeLoginServiceMock! - private var appMediator: AppMediatorMock! + var linkMobileProgressSubject: CurrentValueSubject! + var linkDesktopProgressSubject: CurrentValueSubject! + 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() diff --git a/UnitTests/Sources/RemotePreferenceTests.swift b/UnitTests/Sources/RemotePreferenceTests.swift index 16d00f9b0..df0a8214b 100644 --- a/UnitTests/Sources/RemotePreferenceTests.swift +++ b/UnitTests/Sources/RemotePreferenceTests.swift @@ -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 = .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) } } diff --git a/UnitTests/Sources/ReportContentScreenViewModelTests.swift b/UnitTests/Sources/ReportContentScreenViewModelTests.swift index 6a815ce7d..534c5ec77 100644 --- a/UnitTests/Sources/ReportContentScreenViewModelTests.swift +++ b/UnitTests/Sources/ReportContentScreenViewModelTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/ReportRoomScreenViewModelTests.swift b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift index 4212bf051..f98bb5f85 100644 --- a/UnitTests/Sources/ReportRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index 2a2bd8853..c75be1262 100644 --- a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -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)) } } diff --git a/UnitTests/Sources/RestorationTokenTests.swift b/UnitTests/Sources/RestorationTokenTests.swift index cd21797aa..84e608965 100644 --- a/UnitTests/Sources/RestorationTokenTests.swift +++ b/UnitTests/Sources/RestorationTokenTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift index c40d45f6b..acd18be9a 100644 --- a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift @@ -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, diff --git a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift index 3de3f9e5d..e031bbb1d 100644 --- a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift @@ -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, diff --git a/UnitTests/Sources/RoomDirectorySearchScreenScreenViewModelTests.swift b/UnitTests/Sources/RoomDirectorySearchScreenScreenViewModelTests.swift deleted file mode 100644 index c9783c6fd..000000000 --- a/UnitTests/Sources/RoomDirectorySearchScreenScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/RoomEventStringBuilderTests.swift b/UnitTests/Sources/RoomEventStringBuilderTests.swift index ba22963e6..b30297ca2 100644 --- a/UnitTests/Sources/RoomEventStringBuilderTests.swift +++ b/UnitTests/Sources/RoomEventStringBuilderTests.swift @@ -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 diff --git a/UnitTests/Sources/RoomListFiltersStateTests.swift b/UnitTests/Sources/RoomListFiltersStateTests.swift index bea823626..cc0998909 100644 --- a/UnitTests/Sources/RoomListFiltersStateTests.swift +++ b/UnitTests/Sources/RoomListFiltersStateTests.swift @@ -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 diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index b473aeaa8..151138912 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/RoomMembersFlowCoordinatorTests.swift b/UnitTests/Sources/RoomMembersFlowCoordinatorTests.swift index 2899a40d3..32eb4962c 100644 --- a/UnitTests/Sources/RoomMembersFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomMembersFlowCoordinatorTests.swift @@ -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()), diff --git a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift index ff7b22909..d12986707 100644 --- a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift @@ -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) } } diff --git a/UnitTests/Sources/RoomPermissionsTests.swift b/UnitTests/Sources/RoomPermissionsTests.swift index b08f694ce..910417d18 100644 --- a/UnitTests/Sources/RoomPermissionsTests.swift +++ b/UnitTests/Sources/RoomPermissionsTests.swift @@ -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) } } diff --git a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift index ede24920b..60085ba39 100644 --- a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift @@ -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(), diff --git a/UnitTests/Sources/RoomStateEventStringBuilderTests.swift b/UnitTests/Sources/RoomStateEventStringBuilderTests.swift index a6178adc2..2569a6adf 100644 --- a/UnitTests/Sources/RoomStateEventStringBuilderTests.swift +++ b/UnitTests/Sources/RoomStateEventStringBuilderTests.swift @@ -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)) } } diff --git a/UnitTests/Sources/RoomSummaryProviderTests.swift b/UnitTests/Sources/RoomSummaryProviderTests.swift index 261f739f7..30c639de8 100644 --- a/UnitTests/Sources/RoomSummaryProviderTests.swift +++ b/UnitTests/Sources/RoomSummaryProviderTests.swift @@ -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() diff --git a/UnitTests/Sources/RoomSummaryTests.swift b/UnitTests/Sources/RoomSummaryTests.swift index 1cd47cc2b..b067ee313 100644 --- a/UnitTests/Sources/RoomSummaryTests.swift +++ b/UnitTests/Sources/RoomSummaryTests.swift @@ -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 diff --git a/UnitTests/Sources/RoomTests.swift b/UnitTests/Sources/RoomTests.swift index 748c3923e..422819e51 100644 --- a/UnitTests/Sources/RoomTests.swift +++ b/UnitTests/Sources/RoomTests.swift @@ -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) } } diff --git a/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift deleted file mode 100644 index 6f5149fe7..000000000 --- a/UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift index 0ed688621..27e4a0ca2 100644 --- a/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/SecureBackupLogoutConfirmationScreenViewModelTests.swift @@ -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! + private var secureBackupController: SecureBackupControllerMock + private var reachabilitySubject: CurrentValueSubject - override func setUp() { + init() { secureBackupController = SecureBackupControllerMock() secureBackupController.underlyingKeyBackupState = CurrentValueSubject(.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) diff --git a/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift deleted file mode 100644 index b591112b5..000000000 --- a/UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/SecureBackupScreenViewModelTests.swift b/UnitTests/Sources/SecureBackupScreenViewModelTests.swift deleted file mode 100644 index ed193088c..000000000 --- a/UnitTests/Sources/SecureBackupScreenViewModelTests.swift +++ /dev/null @@ -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 { } diff --git a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift index 4c1b39975..ab40169c8 100644 --- a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift +++ b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift index 63838ad16..bea78a2c4 100644 --- a/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerSelectionScreenViewModelTests.swift @@ -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(), diff --git a/UnitTests/Sources/SessionDirectoriesTests.swift b/UnitTests/Sources/SessionDirectoriesTests.swift index 9da89d9aa..648e59a3f 100644 --- a/UnitTests/Sources/SessionDirectoriesTests.swift +++ b/UnitTests/Sources/SessionDirectoriesTests.swift @@ -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() diff --git a/UnitTests/Sources/SessionVerificationStateMachineTests.swift b/UnitTests/Sources/SessionVerificationStateMachineTests.swift index db84188d3..0ca9d2028 100644 --- a/UnitTests/Sources/SessionVerificationStateMachineTests.swift +++ b/UnitTests/Sources/SessionVerificationStateMachineTests.swift @@ -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) } } diff --git a/UnitTests/Sources/SettingsScreenViewModelTests.swift b/UnitTests/Sources/SettingsScreenViewModelTests.swift index f9a2672da..b1c03f7db 100644 --- a/UnitTests/Sources/SettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/SettingsScreenViewModelTests.swift @@ -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() +@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() diff --git a/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift b/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift index f9fa512b3..c34f91a4e 100644 --- a/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift +++ b/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift b/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift index 15d0f429b..e71642d62 100644 --- a/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceAddRoomsScreenViewModelTests.swift @@ -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.") } } diff --git a/UnitTests/Sources/SpaceListScreenViewModelTests.swift b/UnitTests/Sources/SpaceListScreenViewModelTests.swift index e0733fc7e..06da32429 100644 --- a/UnitTests/Sources/SpaceListScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceListScreenViewModelTests.swift @@ -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) + } } diff --git a/UnitTests/Sources/StartChatViewModelTests.swift b/UnitTests/Sources/StartChatViewModelTests.swift index 1bafa453f..2488b2252 100644 --- a/UnitTests/Sources/StartChatViewModelTests.swift +++ b/UnitTests/Sources/StartChatViewModelTests.swift @@ -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 } diff --git a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift index e69733392..e9933d542 100644 --- a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift +++ b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift @@ -8,21 +8,19 @@ import Combine @testable import ElementX -import XCTest +import Testing @MainActor -class StaticLocationScreenViewModelTests: XCTestCase { - let timelineProxy = TimelineProxyMock(.init()) +@Suite +struct StaticLocationScreenViewModelTests { + private let timelineProxy = TimelineProxyMock(.init()) + private var viewModel: StaticLocationScreenViewModelProtocol - var viewModel: StaticLocationScreenViewModelProtocol! - var context: StaticLocationScreenViewModel.Context { + private var context: StaticLocationScreenViewModel.Context { viewModel.context } - private var cancellables = Set() - - override func setUpWithError() throws { - cancellables.removeAll() + init() { let viewModel = StaticLocationScreenViewModel(interactionMode: .picker, mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration, timelineController: MockTimelineController(timelineProxy: timelineProxy), @@ -32,81 +30,92 @@ class StaticLocationScreenViewModelTests: XCTestCase { self.viewModel = viewModel } - func testUserDidPan() { - XCTAssertTrue(context.viewState.isSharingUserLocation) - XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) + @Test + func userDidPan() { + #expect(context.viewState.isSharingUserLocation) + #expect(context.showsUserLocationMode == .showAndFollow) context.send(viewAction: .userDidPan) - XCTAssertFalse(context.viewState.isSharingUserLocation) - XCTAssertEqual(context.showsUserLocationMode, .show) + #expect(!context.viewState.isSharingUserLocation) + #expect(context.showsUserLocationMode == .show) } - func testCenterOnUser() { - XCTAssertTrue(context.viewState.isSharingUserLocation) + @Test + func centerOnUser() { + #expect(context.viewState.isSharingUserLocation) context.showsUserLocationMode = .show - XCTAssertFalse(context.viewState.isSharingUserLocation) + #expect(!context.viewState.isSharingUserLocation) context.send(viewAction: .centerToUser) - XCTAssertTrue(context.viewState.isSharingUserLocation) - XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) + #expect(context.viewState.isSharingUserLocation) + #expect(context.showsUserLocationMode == .showAndFollow) } - func testCenterOnUserWithoutAuth() { + @Test + func centerOnUserWithoutAuth() { context.showsUserLocationMode = .hide context.isLocationAuthorized = nil context.send(viewAction: .centerToUser) - XCTAssertEqual(context.showsUserLocationMode, .showAndFollow) + #expect(context.showsUserLocationMode == .showAndFollow) } - func testCenterOnUserWithDeniedAuth() { + @Test + func centerOnUserWithDeniedAuth() { context.isLocationAuthorized = false context.showsUserLocationMode = .hide context.send(viewAction: .centerToUser) - XCTAssertNotEqual(context.showsUserLocationMode, .showAndFollow) - XCTAssertNotNil(context.alertInfo) + #expect(context.showsUserLocationMode != .showAndFollow) + #expect(context.alertInfo != nil) } - func testErrorMapping() { + @Test + func errorMapping() { let mapError = AlertInfo(locationSharingViewError: .mapError(.failedLoadingMap)) - XCTAssertEqual(mapError.message, L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName)) + #expect(mapError.message == L10n.errorFailedLoadingMap(InfoPlistReader.main.bundleDisplayName)) let locationError = AlertInfo(locationSharingViewError: .mapError(.failedLocatingUser)) - XCTAssertEqual(locationError.message, L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName)) + #expect(locationError.message == L10n.errorFailedLocatingUser(InfoPlistReader.main.bundleDisplayName)) let authorizationError = AlertInfo(locationSharingViewError: .missingAuthorization) - XCTAssertEqual(authorizationError.message, L10n.dialogPermissionLocationDescriptionIos) + #expect(authorizationError.message == L10n.dialogPermissionLocationDescriptionIos) } - func testSendUserLocation() async throws { + @Test + func sendUserLocation() async throws { context.mapCenterLocation = .init(latitude: 0, longitude: 0) context.geolocationUncertainty = 10 let deferred = deferFulfillment(viewModel.actions) { $0 == .close } - let expectation = XCTestExpectation(description: "sendLocation") - timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in - XCTAssertEqual(geoURI.uncertainty, 10) - XCTAssertEqual(assetType, .sender) - expectation.fulfill() - return .success(()) - } - context.send(viewAction: .selectLocation) - await fulfillment(of: [expectation], timeout: 1) - try await deferred.fulfill() + try await confirmation { confirmation in + timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in + #expect(geoURI.uncertainty == 10) + #expect(assetType == .sender) + confirmation() + return .success(()) + } + + context.send(viewAction: .selectLocation) + + try await deferred.fulfill() + } } - func testSendPickedLocation() async throws { + @Test + func sendPickedLocation() async throws { context.mapCenterLocation = .init(latitude: 0, longitude: 0) context.isLocationAuthorized = nil context.geolocationUncertainty = 10 let deferred = deferFulfillment(viewModel.actions) { $0 == .close } - let expectation = XCTestExpectation(description: "sendLocation") - timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in - XCTAssertEqual(geoURI.uncertainty, nil) - XCTAssertEqual(assetType, .pin) - expectation.fulfill() - return .success(()) - } - context.send(viewAction: .selectLocation) - await fulfillment(of: [expectation], timeout: 1) - try await deferred.fulfill() + try await confirmation { confirmation in + timelineProxy.sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = { _, geoURI, _, _, assetType in + #expect(geoURI.uncertainty == nil) + #expect(assetType == .pin) + confirmation() + return .success(()) + } + + context.send(viewAction: .selectLocation) + + try await deferred.fulfill() + } } } diff --git a/UnitTests/Sources/StringTests.swift b/UnitTests/Sources/StringTests.swift index 15a96825a..bdc4362fb 100644 --- a/UnitTests/Sources/StringTests.swift +++ b/UnitTests/Sources/StringTests.swift @@ -7,79 +7,90 @@ // @testable import ElementX -import XCTest +import Testing -class StringTests: XCTestCase { - func testEmptyIsAscii() { - XCTAssertTrue("".isASCII) +@Suite +struct StringTests { + @Test + func emptyIsAscii() { + #expect("".isASCII) } - func testSpaceIsAscii() { - XCTAssertTrue("".isASCII) + @Test + func spaceIsAscii() { + #expect("".isASCII) } - func testJohnnyIsAscii() { - XCTAssertTrue("johnny".isASCII) + @Test + func johnnyIsAscii() { + #expect("johnny".isASCII) } - func testJöhnnyIsNotAscii() { - XCTAssertFalse("jöhnny".isASCII) + @Test + func jöhnnyIsNotAscii() { + #expect(!"jöhnny".isASCII) } - func testJ🅾️hnnyIsNotAscii() { - XCTAssertFalse("j🅾️hnny".isASCII) + @Test + func jEmojiHnnyIsNotAscii() { + #expect(!"j🅾️hnny".isASCII) } - func testAsciifiedMethod() { + @Test + func asciifiedMethod() { // ASCII strings return themselves unchanged - XCTAssertEqual("johnny".asciified(), "johnny") - XCTAssertEqual("hello".asciified(), "hello") - XCTAssertEqual("abc123".asciified(), "abc123") - XCTAssertEqual("".asciified(), "") - XCTAssertEqual(" ".asciified(), " ") + #expect("johnny".asciified() == "johnny") + #expect("hello".asciified() == "hello") + #expect("abc123".asciified() == "abc123") + #expect("".asciified() == "") + #expect(" ".asciified() == " ") // Non-ASCII strings get converted or stripped - XCTAssertEqual("jöhnny".asciified(), "johnny", "ö should become o") - XCTAssertEqual("jåhnny".asciified(), "jahnny", "å should become a") - XCTAssertEqual("café".asciified(), "cafe") - XCTAssertEqual("naïve".asciified(), "naive") - XCTAssertEqual("résumé".asciified(), "resume") - XCTAssertEqual("🚀".asciified(), "") - XCTAssertEqual("Heartbreak Hotel 🏩".asciified(), "Heartbreak Hotel", "The emoji should be stripped.") - XCTAssertEqual("1️⃣2️⃣3️⃣".asciified(), "123", "The emoji should be converted to ASCII.") + #expect("jöhnny".asciified() == "johnny", "ö should become o") + #expect("jåhnny".asciified() == "jahnny", "å should become a") + #expect("café".asciified() == "cafe") + #expect("naïve".asciified() == "naive") + #expect("résumé".asciified() == "resume") + #expect("🚀".asciified() == "") + #expect("Heartbreak Hotel 🏩".asciified() == "Heartbreak Hotel", "The emoji should be stripped.") + #expect("1️⃣2️⃣3️⃣".asciified() == "123", "The emoji should be converted to ASCII.") } - func testGenerateBreakableWhitespaceEnd() { + @Test + func generateBreakableWhitespaceEnd() { var count = 5 var result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) + #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight) == result) count = 3 result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) + #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight) == result) count = 0 result = "" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) + #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight) == result) count = 4 result = "\u{2067}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) + #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft) == result) count = 0 result = "" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) + #expect(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft) == result) } - func testEllipsizeWorks() { - XCTAssertEqual("ellipsize".ellipsize(length: 5), "ellip…") + @Test + func ellipsizeWorks() { + #expect("ellipsize".ellipsize(length: 5) == "ellip…") } - func testEllipsizeNotNeeded() { - XCTAssertEqual("ellipsize".ellipsize(length: 15), "ellipsize") + @Test + func ellipsizeNotNeeded() { + #expect("ellipsize".ellipsize(length: 15) == "ellipsize") } - func testReplaceBreakOccurrences() { + @Test + func replaceBreakOccurrences() { let input0 = "

" let input1 = "

\n

" let input2 = "

\n\n

" @@ -94,11 +105,11 @@ class StringTests: XCTestCase { let expectedOutput4 = "

a

b

" let expectedOutput5 = input5 - XCTAssertEqual(input0.replacingHtmlBreaksOccurrences(), expectedOutput0) - XCTAssertEqual(input1.replacingHtmlBreaksOccurrences(), expectedOutput1) - XCTAssertEqual(input2.replacingHtmlBreaksOccurrences(), expectedOutput2) - XCTAssertEqual(input3.replacingHtmlBreaksOccurrences(), expectedOutput3) - XCTAssertEqual(input4.replacingHtmlBreaksOccurrences(), expectedOutput4) - XCTAssertEqual(input5.replacingHtmlBreaksOccurrences(), expectedOutput5) + #expect(input0.replacingHtmlBreaksOccurrences() == expectedOutput0) + #expect(input1.replacingHtmlBreaksOccurrences() == expectedOutput1) + #expect(input2.replacingHtmlBreaksOccurrences() == expectedOutput2) + #expect(input3.replacingHtmlBreaksOccurrences() == expectedOutput3) + #expect(input4.replacingHtmlBreaksOccurrences() == expectedOutput4) + #expect(input5.replacingHtmlBreaksOccurrences() == expectedOutput5) } } diff --git a/UnitTests/Sources/TextBasedRoomTimelineTests.swift b/UnitTests/Sources/TextBasedRoomTimelineTests.swift index fa0c4570a..41820ea24 100644 --- a/UnitTests/Sources/TextBasedRoomTimelineTests.swift +++ b/UnitTests/Sources/TextBasedRoomTimelineTests.swift @@ -7,10 +7,13 @@ // @testable import ElementX -import XCTest +import Foundation +import Testing -final class TextBasedRoomTimelineTests: XCTestCase { - func testTextRoomTimelineItemWhitespaceEnd() { +@Suite +struct TextBasedRoomTimelineTests { + @Test + func textRoomTimelineItemWhitespaceEnd() { let timestamp = Calendar.current.startOfDay(for: .now).addingTimeInterval(60 * 60) // 1:00 am let timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, @@ -19,10 +22,11 @@ final class TextBasedRoomTimelineTests: XCTestCase { canBeRepliedTo: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) - XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + 1) + #expect(timelineItem.additionalWhitespaces() == timestamp.formattedTime().count + 1) } - func testTextRoomTimelineItemWhitespaceEndLonger() { + @Test + func textRoomTimelineItemWhitespaceEndLonger() { let timestamp = Calendar.current.startOfDay(for: .now).addingTimeInterval(-60) // 11:59 pm let timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, @@ -31,10 +35,11 @@ final class TextBasedRoomTimelineTests: XCTestCase { canBeRepliedTo: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test")) - XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + 1) + #expect(timelineItem.additionalWhitespaces() == timestamp.formattedTime().count + 1) } - func testTextRoomTimelineItemWhitespaceEndWithEdit() { + @Test + func textRoomTimelineItemWhitespaceEndWithEdit() { let timestamp = Date.mock var timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, @@ -45,10 +50,11 @@ final class TextBasedRoomTimelineTests: XCTestCase { content: .init(body: "Test")) timelineItem.properties.isEdited = true let editedCount = L10n.commonEditedSuffix.count - XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + editedCount + 2) + #expect(timelineItem.additionalWhitespaces() == timestamp.formattedTime().count + editedCount + 2) } - func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() { + @Test + func textRoomTimelineItemWhitespaceEndWithEditAndAlert() { let timestamp = Date.mock var timelineItem = TextRoomTimelineItem(id: .randomEvent, timestamp: timestamp, @@ -60,6 +66,6 @@ final class TextBasedRoomTimelineTests: XCTestCase { timelineItem.properties.isEdited = true timelineItem.properties.deliveryStatus = .sendingFailed(.unknown) let editedCount = L10n.commonEditedSuffix.count - XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.formattedTime().count + editedCount + 5) + #expect(timelineItem.additionalWhitespaces() == timestamp.formattedTime().count + editedCount + 5) } } diff --git a/UnitTests/Sources/TimelineItemFactoryTests.swift b/UnitTests/Sources/TimelineItemFactoryTests.swift index fe9a0cf7d..241becad5 100644 --- a/UnitTests/Sources/TimelineItemFactoryTests.swift +++ b/UnitTests/Sources/TimelineItemFactoryTests.swift @@ -8,11 +8,13 @@ @testable import ElementX import MatrixRustSDK -import XCTest +import Testing @MainActor -class TimelineItemFactoryTests: XCTestCase { - func testCallInvite() { +@Suite +struct TimelineItemFactoryTests { + @Test + func callInvite() throws { let ownUserID = "@alice:matrix.org" let senderUserID = "@bob:matrix.org" @@ -23,20 +25,16 @@ class TimelineItemFactoryTests: XCTestCase { let eventTimelineItem = EventTimelineItem.mockCallInvite(sender: senderUserID) let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init("0")) + + let item = try #require(factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) as? CallInviteRoomTimelineItem, + "Incorrect item type") - let item = factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) - - guard let item = item as? CallInviteRoomTimelineItem else { - XCTFail("Incorrect item type") - return - } - - XCTAssertEqual(item.isReactable, false) - XCTAssertEqual(item.canBeRepliedTo, false) - XCTAssertEqual(item.isEditable, false) - XCTAssertEqual(item.sender, TimelineItemSender(id: senderUserID)) - XCTAssertEqual(item.properties.isEdited, false) - XCTAssertEqual(item.properties.reactions, []) - XCTAssertEqual(item.properties.deliveryStatus, nil) + #expect(item.isReactable == false) + #expect(item.canBeRepliedTo == false) + #expect(item.isEditable == false) + #expect(item.sender == TimelineItemSender(id: senderUserID)) + #expect(item.properties.isEdited == false) + #expect(item.properties.reactions == []) + #expect(item.properties.deliveryStatus == nil) } } diff --git a/UnitTests/Sources/URLComponentsTests.swift b/UnitTests/Sources/URLComponentsTests.swift index e21df5099..aed76e90a 100644 --- a/UnitTests/Sources/URLComponentsTests.swift +++ b/UnitTests/Sources/URLComponentsTests.swift @@ -7,87 +7,67 @@ // @testable import ElementX -import XCTest +import Foundation +import Testing -class URLComponentsTests: XCTestCase { - func testAddFragmentQueryItems() { - guard let url = URL(string: "https://test.matrix.org"), - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - XCTFail("URL invalid") - return - } +@Suite +struct URLComponentsTests { + @Test + func addFragmentQueryItems() throws { + let url = try #require(URL(string: "https://test.matrix.org")) + var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true)) - XCTAssertNil(components.fragmentQueryItems) + #expect(components.fragmentQueryItems == nil) let fragmentQueryItems: [URLQueryItem] = [.init(name: "first", value: "1"), .init(name: "second", value: "2")] components.fragmentQueryItems = fragmentQueryItems - XCTAssertEqual(components.url?.absoluteString, "https://test.matrix.org#?first=1&second=2") + #expect(components.url?.absoluteString == "https://test.matrix.org#?first=1&second=2") } - func testRemoveFragmentQueryItem() { - guard let url = URL(string: "https://test.matrix.org#random/data?first=1&second=2"), - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - XCTFail("URL invalid") - return - } - - XCTAssertNotNil(components.fragmentQueryItems) - guard var fragmentQueryItems = components.fragmentQueryItems else { - return - } + @Test + func removeFragmentQueryItem() throws { + let url = try #require(URL(string: "https://test.matrix.org#random/data?first=1&second=2")) + var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true)) + var fragmentQueryItems = try #require(components.fragmentQueryItems) fragmentQueryItems.removeAll { $0.name == "first" } components.fragmentQueryItems = fragmentQueryItems - XCTAssertEqual(components.url?.absoluteString, "https://test.matrix.org#random/data?second=2") + #expect(components.url?.absoluteString == "https://test.matrix.org#random/data?second=2") } - func testAppendFragmentQueryItem() { - guard let url = URL(string: "https://test.matrix.org#/random/data?first=1&second=2"), - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - XCTFail("URL invalid") - return - } - - XCTAssertNotNil(components.fragmentQueryItems) - guard var fragmentQueryItems = components.fragmentQueryItems else { - return - } + @Test + func appendFragmentQueryItem() throws { + let url = try #require(URL(string: "https://test.matrix.org#/random/data?first=1&second=2")) + var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true)) + var fragmentQueryItems = try #require(components.fragmentQueryItems) fragmentQueryItems.insert(.init(name: "mr in between", value: "hello"), at: 1) components.fragmentQueryItems = fragmentQueryItems - XCTAssertEqual(components.url?.absoluteString, "https://test.matrix.org#/random/data?first=1&mr%20in%20between=hello&second=2") + #expect(components.url?.absoluteString == "https://test.matrix.org#/random/data?first=1&mr%20in%20between=hello&second=2") } - func testChangeFragmentQueryItemValue() { - guard let url = URL(string: "https://test.matrix.org#/random/data?first=1&second=2"), - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - XCTFail("URL invalid") - return - } - - XCTAssertNotNil(components.fragmentQueryItems) - guard var fragmentQueryItems = components.fragmentQueryItems else { - return - } + @Test + func changeFragmentQueryItemValue() throws { + let url = try #require(URL(string: "https://test.matrix.org#/random/data?first=1&second=2")) + var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true)) + var fragmentQueryItems = try #require(components.fragmentQueryItems) fragmentQueryItems[0].value = "last" components.fragmentQueryItems = fragmentQueryItems - XCTAssertEqual(components.url?.absoluteString, "https://test.matrix.org#/random/data?first=last&second=2") + #expect(components.url?.absoluteString == "https://test.matrix.org#/random/data?first=last&second=2") } - func testElementCallParameters() { - guard let url = URL(string: "https://call.element.io/room#/callName?appPrompt=true&confineToRoom=false"), - var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - XCTFail("URL invalid") - return - } + @Test + func elementCallParameters() throws { + let url = try #require(URL(string: "https://call.element.io/room#/callName?appPrompt=true&confineToRoom=false")) + var components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: true)) components.fragmentQueryItems?.removeAll { $0.name == "appPrompt" } components.fragmentQueryItems?.removeAll { $0.name == "confineToRoom" } @@ -97,6 +77,6 @@ class URLComponentsTests: XCTestCase { components.fragmentQueryItems?.append(.init(name: "appPrompt", value: "false")) components.fragmentQueryItems?.append(.init(name: "confineToRoom", value: "true")) - XCTAssertEqual(components.url?.absoluteString, "https://call.element.io/room#/callName?skipLobby=true&appPrompt=false&confineToRoom=true") + #expect(components.url?.absoluteString == "https://call.element.io/room#/callName?skipLobby=true&appPrompt=false&confineToRoom=true") } } diff --git a/UnitTests/Sources/URLTests.swift b/UnitTests/Sources/URLTests.swift index c11790003..123c54dbb 100644 --- a/UnitTests/Sources/URLTests.swift +++ b/UnitTests/Sources/URLTests.swift @@ -7,37 +7,37 @@ // @testable import ElementX -import XCTest +import Foundation +import Testing -class URLTests: XCTestCase { - func testURLDirectoryName() { +@Suite +struct URLTests { + @Test + func urlDirectoryName() throws { let url: URL = "https://matrix.example.com/foo/bar/" let directoryName = url.asDirectoryName() - XCTAssertEqual(directoryName, "matrix.example.com-foo-bar") - createDirectory(with: directoryName) + #expect(directoryName == "matrix.example.com-foo-bar") + try createDirectory(with: directoryName) } - func testComplexURLDirectoryName() { + @Test + func complexURLDirectoryName() throws { let url: URL = "https://us%3Aer:pa%40%3Ass@[2001:db8:85a3::8a2e:370:7334]:8443/..//folder/./fi%20le(1).html;p=1;q=2" let directoryName = url.asDirectoryName() - XCTAssertEqual(directoryName, "us%3Aer-pa%40%3Ass@[2001-db8-85a3--8a2e-370-7334]-8443-..--folder-.-fi%20le(1).html;p=1;q=2") - createDirectory(with: directoryName) + #expect(directoryName == "us%3Aer-pa%40%3Ass@[2001-db8-85a3--8a2e-370-7334]-8443-..--folder-.-fi%20le(1).html;p=1;q=2") + try createDirectory(with: directoryName) } // MARK: - Helpers - func createDirectory(with directoryName: String) { + func createDirectory(with directoryName: String) throws { let url = URL.temporaryDirectory.appending(path: directoryName) try? FileManager.default.removeItem(at: url) - do { - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) - } catch { - XCTFail("Invalid file path: \(error.localizedDescription)") - } + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) guard FileManager.default.directoryExists(at: url) else { - XCTFail("Invalid file path") + Issue.record("Invalid file path") return } } diff --git a/UnitTests/Sources/UserAgentBuilderTests.swift b/UnitTests/Sources/UserAgentBuilderTests.swift index 30f8d7029..92b60e28e 100644 --- a/UnitTests/Sources/UserAgentBuilderTests.swift +++ b/UnitTests/Sources/UserAgentBuilderTests.swift @@ -7,20 +7,24 @@ // @testable import ElementX -import XCTest +import Testing -class UserAgentBuilderTests: XCTestCase { - func testIsNotNil() { - XCTAssertNotNil(UserAgentBuilder.makeASCIIUserAgent()) +@Suite +struct UserAgentBuilderTests { + @Test + func isNotUnknow() { + #expect(UserAgentBuilder.makeASCIIUserAgent() != "unknown") } - func testContainsClientName() { + @Test + func containsClientName() { let userAgent = UserAgentBuilder.makeASCIIUserAgent() - XCTAssert(userAgent.contains(InfoPlistReader.main.bundleDisplayName) == true, "\(userAgent) does not contain client name") + #expect(userAgent.contains(InfoPlistReader.main.bundleDisplayName) == true, "\(userAgent) does not contain client name") } - func testContainsClientVersion() { + @Test + func containsClientVersion() { let userAgent = UserAgentBuilder.makeASCIIUserAgent() - XCTAssert(userAgent.contains(InfoPlistReader.main.bundleShortVersionString) == true, "\(userAgent) does not contain client version") + #expect(userAgent.contains(InfoPlistReader.main.bundleShortVersionString) == true, "\(userAgent) does not contain client version") } } diff --git a/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift b/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift index d7931d574..5f85bbbd7 100644 --- a/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift @@ -8,47 +8,53 @@ import Combine @testable import ElementX -import XCTest +import Testing @MainActor -class UserDetailsEditScreenViewModelTests: XCTestCase { - var viewModel: UserDetailsEditScreenViewModel! +@Suite +struct UserDetailsEditScreenViewModelTests { + private var viewModel: UserDetailsEditScreenViewModel! + private var userIndicatorController: UserIndicatorControllerMock! - var userIndicatorController: UserIndicatorControllerMock! - - var context: UserDetailsEditScreenViewModelType.Context { + private var context: UserDetailsEditScreenViewModelType.Context { viewModel.context } - func testCannotSaveOnLanding() { - setupViewModel() - XCTAssertFalse(context.viewState.canSave) + init() { + userIndicatorController = UserIndicatorControllerMock.default + viewModel = .init(userSession: UserSessionMock(.init()), + mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), + userIndicatorController: userIndicatorController) } - func testNameDidChange() { - setupViewModel() + @Test + func cannotSaveOnLanding() { + #expect(!context.viewState.canSave) + } + + @Test + func nameDidChange() { context.name = "name" - XCTAssertTrue(context.viewState.nameDidChange) - XCTAssertTrue(context.viewState.canSave) + #expect(context.viewState.nameDidChange) + #expect(context.viewState.canSave) } - func testEmptyNameCannotBeSaved() { - setupViewModel() + @Test + func emptyNameCannotBeSaved() { context.name = "" - XCTAssertFalse(context.viewState.canSave) + #expect(!context.viewState.canSave) } - func testAvatarPickerShowsSheet() { - setupViewModel() + @Test + func avatarPickerShowsSheet() { context.name = "name" - XCTAssertFalse(context.showMediaSheet) + #expect(!context.showMediaSheet) context.send(viewAction: .presentMediaSource) - XCTAssertTrue(context.showMediaSheet) + #expect(context.showMediaSheet) } - func testSave() async throws { - setupViewModel() - + @Test + func save() async throws { let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } context.name = "name" @@ -57,43 +63,33 @@ class UserDetailsEditScreenViewModelTests: XCTestCase { try await deferred.fulfill() } - func testCancelWithChangesAndDiscard() async throws { - setupViewModel() + @Test + func cancelWithChangesAndDiscard() async throws { context.name = "name" - XCTAssertTrue(context.viewState.canSave) - XCTAssertNil(context.alertInfo) + #expect(context.viewState.canSave) + #expect(context.alertInfo == nil) context.send(viewAction: .cancel) - XCTAssertNotNil(context.alertInfo) + #expect(context.alertInfo != nil) let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } context.alertInfo?.secondaryButton?.action?() // Discard try await deferred.fulfill() } - func testCancelWithChangesAndSave() async throws { - setupViewModel() + @Test + func cancelWithChangesAndSave() async throws { context.name = "name" - XCTAssertTrue(context.viewState.canSave) - XCTAssertNil(context.alertInfo) + #expect(context.viewState.canSave) + #expect(context.alertInfo == nil) context.send(viewAction: .cancel) - XCTAssertNotNil(context.alertInfo) + #expect(context.alertInfo != nil) let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss } context.alertInfo?.primaryButton.action?() // Save try await deferred.fulfill() } - - // MARK: - Private - - private func setupViewModel() { - userIndicatorController = UserIndicatorControllerMock.default - - viewModel = .init(userSession: UserSessionMock(.init()), - mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings), - userIndicatorController: userIndicatorController) - } } diff --git a/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift b/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift index 6911cd103..b50bf4c55 100644 --- a/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift +++ b/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift @@ -7,85 +7,98 @@ // @testable import ElementX -import XCTest +import Testing @MainActor -class UserDiscoveryServiceTest: XCTestCase { - var service: UserDiscoveryService! - var clientProxy: ClientProxyMock! +@Suite +struct UserDiscoveryServiceTest { + private var service: UserDiscoveryService + private var clientProxy: ClientProxyMock - override func setUpWithError() throws { + private var searchResults: [UserProfileProxy] { + [.mockAlice, .mockBob, .mockCharlie] + } + + init() { clientProxy = .init(.init(userID: "@foo:matrix.org")) service = UserDiscoveryService(clientProxy: clientProxy) } - func testQueryShowingResults() async { + @Test + func queryShowingResults() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: [UserProfileProxy.mockAlice], limited: true)) let results = await (try? search(query: "AAA").get()) ?? [] assertSearchResults(results, toBe: 1) } - func testOwnerIsFiltered() async { + @Test + func ownerIsFiltered() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: [UserProfileProxy(userID: "@foo:matrix.org")], limited: true)) let results = await (try? search(query: "AAA").get()) ?? [] assertSearchResults(results, toBe: 0) } - func testGetProfileIsNotCalled() async { + @Test + func getProfileIsNotCalled() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) clientProxy.profileForReturnValue = .success(.init(userID: "@alice:matrix.org")) let results = await (try? search(query: "AAA").get()) ?? [] assertSearchResults(results, toBe: 3) - XCTAssertFalse(clientProxy.profileForCalled) + #expect(!clientProxy.profileForCalled) } - func testGetProfileIsNotCalledForAccountOwnerID() async { + @Test + func getProfileIsNotCalledForAccountOwnerID() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) clientProxy.profileForReturnValue = .success(.init(userID: "@alice:matrix.org")) let results = await (try? search(query: "foo:matrix.org").get()) ?? [] assertSearchResults(results, toBe: 3) - XCTAssertFalse(clientProxy.profileForCalled) + #expect(!clientProxy.profileForCalled) } - func testLocalResultShows() async { + @Test + func localResultShows() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) clientProxy.profileForReturnValue = .success(.init(userID: "@some:matrix.org")) let results = await (try? search(query: "@a:b.com").get()) ?? [] assertSearchResults(results, toBe: 4) - XCTAssertTrue(clientProxy.profileForCalled) + #expect(clientProxy.profileForCalled) } - func testLocalResultShowsOnSearchError() async { + @Test + func localResultShowsOnSearchError() async { clientProxy.searchUsersSearchTermLimitReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) clientProxy.profileForReturnValue = .success(.init(userID: "@some:matrix.org")) let results = await (try? search(query: "@a:b.com").get()) ?? [] assertSearchResults(results, toBe: 1) - XCTAssertTrue(clientProxy.profileForCalled) + #expect(clientProxy.profileForCalled) } - func testSearchErrorTriggers() async { + @Test + func searchErrorTriggers() async { clientProxy.searchUsersSearchTermLimitReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) clientProxy.profileForReturnValue = .success(.init(userID: "@some:matrix.org")) switch await search(query: "some query") { case .success: - XCTFail("Search users must fail") + Issue.record("Search users must fail") case .failure(let error): - XCTAssertEqual(error, UserDiscoveryErrorType.failedSearchingUsers) + #expect(error == UserDiscoveryErrorType.failedSearchingUsers) } - XCTAssertFalse(clientProxy.profileForCalled) + #expect(!clientProxy.profileForCalled) } - func testLocalResultWithDuplicates() async { + @Test + func localResultWithDuplicates() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) clientProxy.profileForReturnValue = .success(.init(userID: "@bob:matrix.org")) @@ -93,38 +106,31 @@ class UserDiscoveryServiceTest: XCTestCase { assertSearchResults(results, toBe: 3) let firstUserID = results.first?.userID - XCTAssertEqual(firstUserID, "@bob:matrix.org") - XCTAssertTrue(clientProxy.profileForCalled) + #expect(firstUserID == "@bob:matrix.org") + #expect(clientProxy.profileForCalled) } - func testSearchResultsShowWhenGetProfileFails() async { + @Test + func searchResultsShowWhenGetProfileFails() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) clientProxy.profileForReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) let results = await (try? search(query: "@a:b.com").get()) ?? [] let firstUserID = results.first?.userID - XCTAssertEqual(firstUserID, "@a:b.com") - XCTAssertTrue(clientProxy.profileForCalled) + #expect(firstUserID == "@a:b.com") + #expect(clientProxy.profileForCalled) } // MARK: - Private private func assertSearchResults(_ results: [UserProfileProxy], toBe count: Int) { - XCTAssertTrue(count >= 0) - XCTAssertEqual(results.count, count) - XCTAssertEqual(results.isEmpty, count == 0) + #expect(count >= 0) + #expect(results.count == count) + #expect(results.isEmpty == (count == 0)) } private func search(query: String) async -> Result<[UserProfileProxy], UserDiscoveryErrorType> { await service.searchProfiles(with: query) } - - private var searchResults: [UserProfileProxy] { - [ - .mockAlice, - .mockBob, - .mockCharlie - ] - } } diff --git a/UnitTests/Sources/UserIndicatorControllerTests.swift b/UnitTests/Sources/UserIndicatorControllerTests.swift index 18dc7cf6f..98577fa2a 100644 --- a/UnitTests/Sources/UserIndicatorControllerTests.swift +++ b/UnitTests/Sources/UserIndicatorControllerTests.swift @@ -8,40 +8,43 @@ @testable import ElementX import Foundation -import XCTest +import Testing @MainActor -class UserIndicatorControllerTests: XCTestCase { - private var indicatorController: UserIndicatorController! +@Suite +struct UserIndicatorControllerTests { + private var indicatorController: UserIndicatorController - override func setUp() { + init() { indicatorController = UserIndicatorController() } - func testIndicatorQueueing() { + @Test + mutating func indicatorQueueing() { indicatorController.minimumDisplayDuration = 0.0 indicatorController.submitIndicator(.init(id: "First", title: "")) indicatorController.submitIndicator(.init(id: "Second", title: "")) indicatorController.submitIndicator(.init(id: "Third", title: "")) - XCTAssertEqual(indicatorController.indicatorQueue.count, 3) - XCTAssertEqual(indicatorController.indicatorQueue[2].id, "Third") - XCTAssertEqual(indicatorController.indicatorQueue[1].id, "Second") - XCTAssertEqual(indicatorController.indicatorQueue[0].id, "First") + #expect(indicatorController.indicatorQueue.count == 3) + #expect(indicatorController.indicatorQueue[2].id == "Third") + #expect(indicatorController.indicatorQueue[1].id == "Second") + #expect(indicatorController.indicatorQueue[0].id == "First") indicatorController.retractIndicatorWithId("Second") - XCTAssertEqual(indicatorController.indicatorQueue.count, 2) - XCTAssertEqual(indicatorController.indicatorQueue[1].id, "Third") - XCTAssertEqual(indicatorController.indicatorQueue[0].id, "First") + #expect(indicatorController.indicatorQueue.count == 2) + #expect(indicatorController.indicatorQueue[1].id == "Third") + #expect(indicatorController.indicatorQueue[0].id == "First") indicatorController.retractAllIndicators() - XCTAssertEqual(indicatorController.indicatorQueue.count, 0) + #expect(indicatorController.indicatorQueue.count == 0) } - func testChainedPresentation() async throws { + @Test + mutating func chainedPresentation() async throws { indicatorController.minimumDisplayDuration = 0.25 indicatorController.nonPersistentDisplayDuration = 2.5 @@ -49,19 +52,21 @@ class UserIndicatorControllerTests: XCTestCase { indicatorController.submitIndicator(.init(id: "Second", title: "")) indicatorController.submitIndicator(.init(id: "Third", title: "")) - XCTAssertEqual(indicatorController.activeIndicator?.id, "Third") + #expect(indicatorController.activeIndicator?.id == "Third") - let fulfillment = deferFulfillment(indicatorController.$activeIndicator, message: "Waiting for last indicator to be dismissed") { indicator in + let fulfillment = deferFulfillment(indicatorController.$activeIndicator, + message: "Waiting for last indicator to be dismissed") { indicator in indicator?.id == "Second" } try await fulfillment.fulfill() - XCTAssertEqual(indicatorController.indicatorQueue.count, 2) - XCTAssertEqual(indicatorController.activeIndicator?.id, "Second") + #expect(indicatorController.indicatorQueue.count == 2) + #expect(indicatorController.activeIndicator?.id == "Second") } - func testMinimumDisplayDuration() async throws { + @Test + mutating func minimumDisplayDuration() async throws { indicatorController.minimumDisplayDuration = 0.25 indicatorController.nonPersistentDisplayDuration = 2.5 @@ -69,9 +74,10 @@ class UserIndicatorControllerTests: XCTestCase { indicatorController.submitIndicator(.init(id: "Second", title: "")) indicatorController.submitIndicator(.init(id: "Third", title: "")) - XCTAssertEqual(indicatorController.indicatorQueue.count, 3) + #expect(indicatorController.indicatorQueue.count == 3) - var fulfillment = deferFulfillment(indicatorController.$activeIndicator, message: "Waiting for minimum display duration to pass") { indicator in + var fulfillment = deferFulfillment(indicatorController.$activeIndicator, + message: "Waiting for minimum display duration to pass") { indicator in indicator?.id == "First" } @@ -79,16 +85,17 @@ class UserIndicatorControllerTests: XCTestCase { try await fulfillment.fulfill() - XCTAssertEqual(indicatorController.indicatorQueue.count, 1) - XCTAssertEqual(indicatorController.activeIndicator?.id, "First") + #expect(indicatorController.indicatorQueue.count == 1) + #expect(indicatorController.activeIndicator?.id == "First") - fulfillment = deferFulfillment(indicatorController.$activeIndicator, message: "Waiting for last indicator to be dismissed") { indicator in + fulfillment = deferFulfillment(indicatorController.$activeIndicator, + message: "Waiting for last indicator to be dismissed") { indicator in indicator == nil } try await fulfillment.fulfill() - XCTAssertEqual(indicatorController.indicatorQueue.count, 0) - XCTAssertNil(indicatorController.activeIndicator) + #expect(indicatorController.indicatorQueue.count == 0) + #expect(indicatorController.activeIndicator == nil) } } diff --git a/UnitTests/Sources/UserPreferenceTests.swift b/UnitTests/Sources/UserPreferenceTests.swift index 9da110f5a..f5ff15a7f 100644 --- a/UnitTests/Sources/UserPreferenceTests.swift +++ b/UnitTests/Sources/UserPreferenceTests.swift @@ -8,15 +8,17 @@ @testable import ElementX import Foundation -import XCTest +import Testing -final class UserPreferenceTests: XCTestCase { - override func setUpWithError() throws { +@Suite +struct UserPreferenceTests { + init() { UserDefaults.testDefaults.removeVolatileDomain(forName: .userDefaultsSuiteName) UserDefaults.testDefaults.removePersistentDomain(forName: .userDefaultsSuiteName) } - func testStorePlistValue() { + @Test + func storePlistValue() { let setPreference = { let value = TestPreferences() value.plist = "Hello" @@ -26,12 +28,13 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() - XCTAssertEqual(value.plist, "Hello") - XCTAssertNotNil(UserDefaults.testDefaults.string(forKey: .key2), "Hello") - XCTAssertNil(UserDefaults.testDefaults.data(forKey: .key2), "Hello") + #expect(value.plist == "Hello") + #expect(UserDefaults.testDefaults.string(forKey: .key2) != nil) + #expect(UserDefaults.testDefaults.data(forKey: .key2) == nil) } - func testStoreCodableValue() { + @Test + func storeCodableValue() { let storedType = CodableTestType(a: "some", b: [1, 2, 3]) let setPreference = { @@ -43,11 +46,12 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() - XCTAssertEqual(value.codable, storedType) - XCTAssertNotNil(UserDefaults.testDefaults.data(forKey: .key3)) + #expect(value.codable == storedType) + #expect(UserDefaults.testDefaults.data(forKey: .key3) != nil) } - func testStorePlistValueOnVolatileStorage() { + @Test + func storePlistValueOnVolatileStorage() { let setPreference = { let value = TestPreferences() value.volatileVar = "Hello" @@ -57,10 +61,11 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() - XCTAssertNil(value.volatileVar) + #expect(value.volatileVar == nil) } - func testStoreCodableValueOnVolatileStorage() { + @Test + func storeCodableValueOnVolatileStorage() { let storedType = CodableTestType(a: "some", b: [1, 2, 3]) let setPreference = { @@ -72,11 +77,12 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() - XCTAssertNil(value.volatileCodable) - XCTAssertNil(UserDefaults.testDefaults.data(forKey: .key4)) + #expect(value.volatileCodable == nil) + #expect(UserDefaults.testDefaults.data(forKey: .key4) == nil) } - func testStorePlistArray() { + @Test + func storePlistArray() { let setPreference = { let value = TestPreferences() value.plistArray = [1, 2, 3] @@ -86,12 +92,13 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() - XCTAssertEqual(value.plistArray, [1, 2, 3]) - XCTAssertEqual(UserDefaults.testDefaults.array(forKey: .key5) as? [Int], [1, 2, 3]) - XCTAssertNil(UserDefaults.testDefaults.data(forKey: .key5), "Hello") + #expect(value.plistArray == [1, 2, 3]) + #expect(UserDefaults.testDefaults.array(forKey: .key5) as? [Int] == [1, 2, 3]) + #expect(UserDefaults.testDefaults.data(forKey: .key5) == nil) } - func testAssignNilToPlistType() { + @Test + func assignNilToPlistType() { let setPreference = { let value = TestPreferences() value.plist = "Hello" @@ -102,11 +109,12 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() value.plist = nil - XCTAssertNil(value.plist) - XCTAssertNil(UserDefaults.testDefaults.string(forKey: .key2)) + #expect(value.plist == nil) + #expect(UserDefaults.testDefaults.string(forKey: .key2) == nil) } - func testAssignNilToCodableType() { + @Test + func assignNilToCodableType() { let storedType = CodableTestType(a: "some", b: [1, 2, 3]) let setPreference = { @@ -119,31 +127,33 @@ final class UserPreferenceTests: XCTestCase { let value = TestPreferences() value.codable = nil - XCTAssertNil(value.codable) - XCTAssertNil(UserDefaults.testDefaults.data(forKey: .key3)) + #expect(value.codable == nil) + #expect(UserDefaults.testDefaults.data(forKey: .key3) == nil) } - func testLocalOverRemoteValue() { + @Test + func localOverRemoteValue() { @UserPreference(key: "testKey", defaultValue: "", storageType: .userDefaults(.testDefaults)) var preference - XCTAssertEqual(preference, "") + #expect(preference == "") _preference.remoteValue = "remote" - XCTAssertEqual(preference, "remote") + #expect(preference == "remote") preference = "local" - XCTAssertEqual(preference, "local") + #expect(preference == "local") } - func testRemoteOverLocalValue() { + @Test + func remoteOverLocalValue() { @UserPreference(key: "testKey", defaultValue: "", storageType: .userDefaults(.testDefaults), mode: .remoteOverLocal) var preference - XCTAssertEqual(preference, "") + #expect(preference == "") _preference.remoteValue = "remote" - XCTAssertEqual(preference, "remote") + #expect(preference == "remote") preference = "local" - XCTAssertEqual(preference, "remote") - XCTAssertTrue(_preference.isLockedToRemote) + #expect(preference == "remote") + #expect(_preference.isLockedToRemote) } } diff --git a/UnitTests/Sources/UserProfileScreenViewModelTests.swift b/UnitTests/Sources/UserProfileScreenViewModelTests.swift index 0db8154a1..5961fb517 100644 --- a/UnitTests/Sources/UserProfileScreenViewModelTests.swift +++ b/UnitTests/Sources/UserProfileScreenViewModelTests.swift @@ -7,50 +7,50 @@ // @testable import ElementX -import XCTest +import Testing @MainActor -class UserProfileScreenViewModelTests: XCTestCase { - var viewModel: UserProfileScreenViewModel! - var context: UserProfileScreenViewModelType.Context { - viewModel.context - } - - func testInitialState() async throws { +@Suite +struct UserProfileScreenViewModelTests { + @Test + func initialState() async throws { let profile = UserProfileProxy(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) - viewModel = UserProfileScreenViewModel(userID: profile.userID, - isPresentedModally: false, - userSession: UserSessionMock(.init(clientProxy: clientProxy)), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - analytics: ServiceLocator.shared.analytics) + let viewModel = UserProfileScreenViewModel(userID: profile.userID, + isPresentedModally: false, + userSession: UserSessionMock(.init(clientProxy: clientProxy)), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + analytics: ServiceLocator.shared.analytics) + let context = viewModel.context let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil } try await waitForMemberToLoad.fulfill() - XCTAssertFalse(context.viewState.isOwnUser) - XCTAssertEqual(context.viewState.userProfile, profile) - XCTAssertNotNil(context.viewState.permalink) + #expect(!context.viewState.isOwnUser) + #expect(context.viewState.userProfile == profile) + #expect(context.viewState.permalink != nil) } - func testInitialStateAccountOwner() async throws { + @Test + func initialStateAccountOwner() async throws { let profile = UserProfileProxy(userID: RoomMemberProxyMock.mockMe.userID, displayName: "Me", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) - viewModel = UserProfileScreenViewModel(userID: profile.userID, - isPresentedModally: false, - userSession: UserSessionMock(.init(clientProxy: clientProxy)), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - analytics: ServiceLocator.shared.analytics) + let viewModel = UserProfileScreenViewModel(userID: profile.userID, + isPresentedModally: false, + userSession: UserSessionMock(.init(clientProxy: clientProxy)), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + analytics: ServiceLocator.shared.analytics) + let context = viewModel.context let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil } try await waitForMemberToLoad.fulfill() - XCTAssertTrue(context.viewState.isOwnUser) - XCTAssertEqual(context.viewState.userProfile, profile) - XCTAssertNotNil(context.viewState.permalink) + #expect(context.viewState.isOwnUser) + #expect(context.viewState.userProfile == profile) + #expect(context.viewState.permalink != nil) } } diff --git a/UnitTests/Sources/UserSession/UserSessionTests.swift b/UnitTests/Sources/UserSession/UserSessionTests.swift deleted file mode 100644 index d3269b516..000000000 --- a/UnitTests/Sources/UserSession/UserSessionTests.swift +++ /dev/null @@ -1,12 +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. -// -import Combine -@testable import ElementX -import XCTest - -final class UserSessionTests: XCTestCase { } diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 188603685..34824f1c9 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -8,38 +8,38 @@ import Combine @testable import ElementX -import XCTest +import Foundation +import Testing @MainActor -class UserSessionFlowCoordinatorTests: XCTestCase { - var userSessionFlowCoordinator: UserSessionFlowCoordinator! - var rootCoordinator: NavigationRootCoordinator! - var userIndicatorController: UserIndicatorControllerMock! - let stateMachineFactory = PublishedStateMachineFactory() +@Suite +struct UserSessionFlowCoordinatorTests { + private var userSessionFlowCoordinator: UserSessionFlowCoordinator! + private var rootCoordinator: NavigationRootCoordinator! + private var userIndicatorController: UserIndicatorControllerMock! + private let stateMachineFactory = PublishedStateMachineFactory() - let networkReachabilitySubject: CurrentValueSubject = .init(.reachable) - let homeserverReachabilitySubject: CurrentValueSubject = .init(.reachable) - var cancellables = Set() + private let networkReachabilitySubject: CurrentValueSubject = .init(.reachable) + private let homeserverReachabilitySubject: CurrentValueSubject = .init(.reachable) + private var cancellables = Set() - var tabCoordinator: NavigationTabCoordinator? { + private var tabCoordinator: NavigationTabCoordinator? { rootCoordinator?.rootCoordinator as? NavigationTabCoordinator } - - var chatsSplitCoordinator: NavigationSplitCoordinator? { + + private var chatsSplitCoordinator: NavigationSplitCoordinator? { tabCoordinator?.tabCoordinators.first as? NavigationSplitCoordinator } - - var detailCoordinator: CoordinatorProtocol? { + + private var detailCoordinator: CoordinatorProtocol? { chatsSplitCoordinator?.detailCoordinator } - - var detailNavigationStack: NavigationStackCoordinator? { + + private var detailNavigationStack: NavigationStackCoordinator? { detailCoordinator as? NavigationStackCoordinator } - override func setUp() async throws { - cancellables.removeAll() - + init() async throws { rootCoordinator = NavigationRootCoordinator() let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) @@ -76,156 +76,166 @@ class UserSessionFlowCoordinatorTests: XCTestCase { // MARK: Navigation - func testInitialState() { - XCTAssertNotNil(chatsSplitCoordinator) - XCTAssertNil(detailCoordinator) + @Test + func initialState() { + #expect(chatsSplitCoordinator != nil) + #expect(detailCoordinator == nil) } - func testSettingsPresentation() async throws { + @Test + mutating func settingsPresentation() async throws { try await process(route: .settings, expectedUserSessionState: .settingsScreen) - XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + #expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) } - func testRoomPresentation() async throws { + @Test + mutating func roomPresentation() async throws { try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNotNil(detailCoordinator) + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(detailCoordinator != nil) } - func testRoomPresentationClearsSettings() async throws { + @Test + mutating func roomPresentationClearsSettings() async throws { try await process(route: .settings, expectedUserSessionState: .settingsScreen) - XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) - XCTAssertNil(detailCoordinator) + #expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + #expect(detailCoordinator == nil) try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) - XCTAssertNil((tabCoordinator?.sheetCoordinator)) - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNotNil(detailCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(detailCoordinator != nil) } - func testChildRoomPresentation() async throws { + @Test + mutating func childRoomPresentation() async throws { try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) - let detailNavigationStack = try XCTUnwrap(detailNavigationStack, "There must be a navigation stack.") - XCTAssertTrue(detailNavigationStack.rootCoordinator is RoomScreenCoordinator) - XCTAssertNotNil(detailCoordinator) + let detailNavigationStack = try #require(detailNavigationStack, "There must be a navigation stack.") + #expect(detailNavigationStack.rootCoordinator is RoomScreenCoordinator) + #expect(detailCoordinator != nil) let deferred = deferFulfillment(detailNavigationStack.observe(\.stackCoordinators.count)) { $0 == 1 } try await process(route: .childRoom(roomID: "2", via: [])) try await deferred.fulfill() - XCTAssertTrue(detailNavigationStack.rootCoordinator is RoomScreenCoordinator) - XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(detailNavigationStack.stackCoordinators.count, 1) - XCTAssertTrue(detailNavigationStack.stackCoordinators.first is RoomScreenCoordinator) + #expect(detailNavigationStack.rootCoordinator is RoomScreenCoordinator) + #expect(detailCoordinator != nil) + #expect(detailNavigationStack.stackCoordinators.count == 1) + #expect(detailNavigationStack.stackCoordinators.first is RoomScreenCoordinator) } - func testShareMediaRouteWithoutRoom() async throws { + @Test + mutating func shareMediaRouteWithoutRoom() async throws { try await process(route: .settings, expectedUserSessionState: .settingsScreen) - XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) - XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) - + #expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + #expect(chatsSplitCoordinator?.sheetCoordinator == nil) + let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: nil, mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)]) try await process(route: .share(sharePayload), expectedUserSessionState: .tabBar, expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload)) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) } - func testShareMediaRouteWithRoom() async throws { + @Test + mutating func shareMediaRouteWithRoom() async throws { try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) - + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect(chatsSplitCoordinator?.sheetCoordinator == nil) + let sharePayload: ShareExtensionPayload = .mediaFiles(roomID: "2", mediaFiles: [.init(url: .picturesDirectory, suggestedName: nil)]) try await process(route: .share(sharePayload), expectedChatsState: .roomList(detailState: .room(roomID: "2"))) - - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) } - func testShareTextRouteWithoutRoom() async throws { + @Test + mutating func shareTextRouteWithoutRoom() async throws { try await process(route: .settings, expectedUserSessionState: .settingsScreen) - XCTAssertTrue((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) - XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) - + #expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + #expect(chatsSplitCoordinator?.sheetCoordinator == nil) + let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text") try await process(route: .share(sharePayload), expectedUserSessionState: .tabBar, expectedChatsState: .shareExtensionRoomList(sharePayload: sharePayload)) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertTrue((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) } - func testShareTextRouteWithRoom() async throws { + @Test + mutating func shareTextRouteWithRoom() async throws { try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1"))) - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator) - + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect(chatsSplitCoordinator?.sheetCoordinator == nil) + let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text") try await process(route: .share(sharePayload), expectedChatsState: .roomList(detailState: .room(roomID: "2"))) - - XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) - XCTAssertNil(tabCoordinator?.sheetCoordinator) - XCTAssertNil(chatsSplitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.") + + #expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + #expect(tabCoordinator?.sheetCoordinator == nil) + #expect(chatsSplitCoordinator?.sheetCoordinator == nil, "The media upload sheet shouldn't be shown when sharing text.") } // MARK: Indicators - func testReachabilityIndicators() async throws { + @Test + func reachabilityIndicators() async throws { // Given a flow in its initial state. try await Task.sleep(for: .milliseconds(100)) // Then no reachability indicators should be shown. - XCTAssertFalse(userIndicatorController.submitIndicatorDelayCalled) - XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) // The initial state removes the indicator. + #expect(!userIndicatorController.submitIndicatorDelayCalled) + #expect(retractReachabilityIndicatorCallsCount == 1) // The initial state removes the indicator. // When the homeserver becomes unreachable. homeserverReachabilitySubject.send(.unreachable) try await Task.sleep(for: .milliseconds(100)) // Then a server unreachable indicator should be shown. - XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 1) - XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonServerUnreachable) - XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) + #expect(userIndicatorController.submitIndicatorDelayCallsCount == 1) + #expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonServerUnreachable) + #expect(retractReachabilityIndicatorCallsCount == 1) // When the network also becomes unreachable. networkReachabilitySubject.send(.unreachable) try await Task.sleep(for: .milliseconds(100)) // Then the server unreachable indicator should be replaced with an offline indicator. - XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 2) - XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonOffline) - XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) + #expect(userIndicatorController.submitIndicatorDelayCallsCount == 2) + #expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonOffline) + #expect(retractReachabilityIndicatorCallsCount == 1) // When the homeserver becomes reachable again. homeserverReachabilitySubject.send(.reachable) try await Task.sleep(for: .milliseconds(100)) // Then there should still be an offline indicator (as we don't yet support air-gapped servers on iOS). - XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3) - XCTAssertEqual(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title, L10n.commonOffline) - XCTAssertEqual(retractReachabilityIndicatorCallsCount, 1) + #expect(userIndicatorController.submitIndicatorDelayCallsCount == 3) + #expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonOffline) + #expect(retractReachabilityIndicatorCallsCount == 1) // When the network becomes reachable again. networkReachabilitySubject.send(.reachable) try await Task.sleep(for: .milliseconds(100)) // Then the indicator should be hidden now as everything is back to normal - XCTAssertEqual(userIndicatorController.submitIndicatorDelayCallsCount, 3) - XCTAssertEqual(retractReachabilityIndicatorCallsCount, 2) + #expect(userIndicatorController.submitIndicatorDelayCallsCount == 3) + #expect(retractReachabilityIndicatorCallsCount == 2) } // MARK: - Helpers - private func process(route: AppRoute, - expectedUserSessionState: UserSessionFlowCoordinator.State? = nil, - expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) async throws { - let deferredUserSession: DeferredFulfillment? = if let expectedUserSessionState { + private mutating func process(route: AppRoute, + expectedUserSessionState: UserSessionFlowCoordinator.State? = nil, + expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) async throws { + let deferredUserSession: DeferredFulfillment? = if let expectedUserSessionState { deferFulfillment(stateMachineFactory.userSessionFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) { $0 == expectedUserSessionState } @@ -233,7 +243,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { nil } - let deferredChatsState: DeferredFulfillment? = if let expectedChatsState { + let deferredChatsState: DeferredFulfillment? = if let expectedChatsState { deferFulfillment(stateMachineFactory.chatsTabFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) { $0 == expectedChatsState } diff --git a/UnitTests/Sources/VoiceMessageCacheTests.swift b/UnitTests/Sources/VoiceMessageCacheTests.swift index aa86ca95f..296e7345a 100644 --- a/UnitTests/Sources/VoiceMessageCacheTests.swift +++ b/UnitTests/Sources/VoiceMessageCacheTests.swift @@ -9,20 +9,21 @@ import Combine @testable import ElementX import Foundation -import XCTest +import Testing @MainActor -class VoiceMessageCacheTests: XCTestCase { - private var voiceMessageCache: VoiceMessageCache! - private var mediaSource: MediaSourceProxy! - private var fileManager: FileManager! +@Suite +final class VoiceMessageCacheTests { + private var voiceMessageCache: VoiceMessageCache + private var mediaSource: MediaSourceProxy + private let fileManager: FileManager private let someURL = URL.mockMXCAudio private let testFilename = "test-file" private let mpeg4aacFileExtension = "m4a" private let testTemporaryDirectory = URL.temporaryDirectory.appendingPathComponent("test-voice-messsage-cache") - override func setUp() async throws { + init() throws { voiceMessageCache = VoiceMessageCache() voiceMessageCache.clearCache() @@ -33,61 +34,63 @@ class VoiceMessageCacheTests: XCTestCase { try fileManager.createDirectory(at: testTemporaryDirectory, withIntermediateDirectories: true) } - override func tearDown() async throws { + deinit { voiceMessageCache.clearCache() - - // clear the test temporary directory - try fileManager.removeItem(at: testTemporaryDirectory) + try? fileManager.removeItem(at: testTemporaryDirectory) } - func testFileURL() throws { + @Test + func fileURL() throws { // If the file is not already in the cache, no URL is expected - XCTAssertNil(voiceMessageCache.fileURL(for: mediaSource)) + #expect(voiceMessageCache.fileURL(for: mediaSource) == nil) // If the file is present in the cache, its URL must be returned let temporaryFileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension) guard case .success(let cachedURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: temporaryFileURL, move: true) else { - XCTFail("A success is expected") + Issue.record("A success is expected") return } - XCTAssertEqual(cachedURL, voiceMessageCache.fileURL(for: mediaSource)) + #expect(cachedURL == voiceMessageCache.fileURL(for: mediaSource)) } - func testCacheInvalidFileExtension() throws { + @Test + func cacheInvalidFileExtension() throws { // An error should be raised if the file extension is not "m4a" let mpegFileURL = try createTemporaryFile(named: testFilename, withExtension: "mpg") guard case .failure(let error) = voiceMessageCache.cache(mediaSource: mediaSource, using: mpegFileURL, move: true) else { - XCTFail("An error is expected") + Issue.record("An error is expected") return } - XCTAssertEqual(error, .invalidFileExtension) + #expect(error == .invalidFileExtension) } - func testCacheCopy() throws { + @Test + func cacheCopy() throws { let fileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension) guard case .success(let cacheURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: false) else { - XCTFail("A success is expected") + Issue.record("A success is expected") return } // The source file must remain in its original location - XCTAssertTrue(fileManager.fileExists(atPath: fileURL.path())) + #expect(fileManager.fileExists(atPath: fileURL.path())) // A copy must be present in the cache - XCTAssertTrue(fileManager.fileExists(atPath: cacheURL.path())) + #expect(fileManager.fileExists(atPath: cacheURL.path())) } - func testCacheMove() throws { + @Test + func cacheMove() throws { let fileURL = try createTemporaryFile(named: testFilename, withExtension: mpeg4aacFileExtension) guard case .success(let cacheURL) = voiceMessageCache.cache(mediaSource: mediaSource, using: fileURL, move: true) else { - XCTFail("A success is expected") + Issue.record("A success is expected") return } // The file must have been moved - XCTAssertFalse(fileManager.fileExists(atPath: fileURL.path())) - XCTAssertTrue(fileManager.fileExists(atPath: cacheURL.path())) + #expect(!fileManager.fileExists(atPath: fileURL.path())) + #expect(fileManager.fileExists(atPath: cacheURL.path())) } private func createTemporaryFile(named filename: String, withExtension fileExtension: String) throws -> URL { diff --git a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift index db5d34500..30af8ffee 100644 --- a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift +++ b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift @@ -9,18 +9,19 @@ import Combine @testable import ElementX import Foundation -import XCTest +import Testing @MainActor -class VoiceMessageMediaManagerTests: XCTestCase { - private var voiceMessageMediaManager: VoiceMessageMediaManager! - private var voiceMessageCache: VoiceMessageCacheMock! - private var mediaProvider: MediaProviderMock! +@Suite +struct VoiceMessageMediaManagerTests { + private var voiceMessageMediaManager: VoiceMessageMediaManager + private var voiceMessageCache: VoiceMessageCacheMock + private var mediaProvider: MediaProviderMock private let someURL = URL.mockMXCAudio private let audioOGGMimeType = "audio/ogg" - override func setUp() async throws { + init() { voiceMessageCache = VoiceMessageCacheMock() mediaProvider = MediaProviderMock(configuration: .init()) mediaProvider.loadFileFromSourceFilenameClosure = nil @@ -29,23 +30,25 @@ class VoiceMessageMediaManagerTests: XCTestCase { voiceMessageCache: voiceMessageCache) } - func testLoadVoiceMessageFromSourceUnsupportedMedia() async throws { + @Test + func loadVoiceMessageFromSourceUnsupportedMedia() async throws { // Only "audio/ogg" file are supported let unsupportedMediaSource = try MediaSourceProxy(url: someURL, mimeType: "audio/wav") do { _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(unsupportedMediaSource, body: nil) - XCTFail("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") + Issue.record("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") } catch { switch error as? VoiceMessageMediaManagerError { case .unsupportedMimeTye: break default: - XCTFail("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") + Issue.record("A `VoiceMessageMediaManagerError.unsupportedMimeTye` error is expected") } } } - func testLoadVoiceMessageFromSourceMimeTypeWithParameters() async throws { + @Test + mutating func loadVoiceMessageFromSourceMimeTypeWithParameters() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file.ogg") // URL representing the final cached file @@ -63,41 +66,44 @@ class VoiceMessageMediaManagerTests: XCTestCase { do { _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) } catch { - XCTFail("An unexpected error has occured: \(error)") + Issue.record("An unexpected error has occured: \(error)") } } - func testLoadVoiceMessageFromSourceAlreadyCached() async throws { + @Test + func loadVoiceMessageFromSourceAlreadyCached() async throws { // Check if the file is already present in cache voiceMessageCache.fileURLForReturnValue = URL("/converted_file/url") let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) - XCTAssertEqual(url, URL("/converted_file/url")) + #expect(url == URL("/converted_file/url")) // The file must have be search in the cache - XCTAssertTrue(voiceMessageCache.fileURLForCalled) - XCTAssertEqual(voiceMessageCache.fileURLForReceivedMediaSource, mediaSource) + #expect(voiceMessageCache.fileURLForCalled) + #expect(voiceMessageCache.fileURLForReceivedMediaSource == mediaSource) // The file must not have been cached again - XCTAssertFalse(voiceMessageCache.cacheMediaSourceUsingMoveCalled) + #expect(!voiceMessageCache.cacheMediaSourceUsingMoveCalled) } - func testLoadVoiceMessageFromSourceMediaProviderError() async throws { + @Test + func loadVoiceMessageFromSourceMediaProviderError() async throws { // An error must be reported if the file cannot be retrieved do { voiceMessageCache.fileURLForReturnValue = nil let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) _ = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) - XCTFail("A `MediaProviderError.failedRetrievingFile` error is expected") + Issue.record("A `MediaProviderError.failedRetrievingFile` error is expected") } catch { switch error as? MediaProviderError { case .failedRetrievingFile: break default: - XCTFail("A `MediaProviderError.failedRetrievingFile` error is expected") + Issue.record("A `MediaProviderError.failedRetrievingFile` error is expected") } } } - func testLoadVoiceMessageFromSourceSingleCall() async throws { + @Test + mutating func loadVoiceMessageFromSourceSingleCall() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file") // URL representing the final cached file @@ -115,17 +121,18 @@ class VoiceMessageMediaManagerTests: XCTestCase { let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) // The file must have been converted - XCTAssertTrue(audioConverter.convertToMPEG4AACSourceURLDestinationURLCalled) + #expect(audioConverter.convertToMPEG4AACSourceURLDestinationURLCalled) // The converted file must have been cached - XCTAssert(voiceMessageCache.cacheMediaSourceUsingMoveCalled) - XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.mediaSource, mediaSource) - XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.fileURL.pathExtension, "m4a") - XCTAssertTrue(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.move ?? false) + #expect(voiceMessageCache.cacheMediaSourceUsingMoveCalled) + #expect(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.mediaSource == mediaSource) + #expect(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.fileURL.pathExtension == "m4a") + #expect(voiceMessageCache.cacheMediaSourceUsingMoveReceivedArguments?.move ?? false) // The returned URL must point to the cached converted file - XCTAssertEqual(url, cachedConvertedFileURL) + #expect(url == cachedConvertedFileURL) } - func testLoadVoiceMessageFromSourceMultipleCalls() async throws { + @Test + mutating func loadVoiceMessageFromSourceMultipleCalls() async throws { // URL representing the file loaded by the media provider let loadedFile = URL("/some/url/loaded_file") // URL representing the final cached file @@ -151,13 +158,13 @@ class VoiceMessageMediaManagerTests: XCTestCase { let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) for _ in 0..<10 { let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) - XCTAssertEqual(url, cachedConvertedFileURL) + #expect(url == cachedConvertedFileURL) } // The file must have been converted only once - XCTAssertEqual(audioConverter.convertToMPEG4AACSourceURLDestinationURLCallsCount, 1) + #expect(audioConverter.convertToMPEG4AACSourceURLDestinationURLCallsCount == 1) // The converted file must have been cached only once - XCTAssertEqual(voiceMessageCache.cacheMediaSourceUsingMoveCallsCount, 1) + #expect(voiceMessageCache.cacheMediaSourceUsingMoveCallsCount == 1) } } diff --git a/UnitTests/SupportingFiles/UnitTests.xctestplan b/UnitTests/SupportingFiles/UnitTests.xctestplan index d7c911081..9000975c3 100644 --- a/UnitTests/SupportingFiles/UnitTests.xctestplan +++ b/UnitTests/SupportingFiles/UnitTests.xctestplan @@ -21,6 +21,7 @@ }, "testTargets" : [ { + "parallelizable" : false, "target" : { "containerPath" : "container:ElementX.xcodeproj", "identifier" : "32C23C8D224D46EFE62AFAD0", diff --git a/compound-ios/Tests/CompoundTests/PreviewTests.swift b/compound-ios/Tests/CompoundTests/PreviewTests.swift index c07e7804c..84d7e7afc 100644 --- a/compound-ios/Tests/CompoundTests/PreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/PreviewTests.swift @@ -12,7 +12,7 @@ import Combine import SwiftUI import Testing -@Suite(.serialized) +@Suite struct PreviewTests { private struct SnapshotDevice { let name: String