From 173b39a07f564fc7f6e1c19c49c87486c334fb23 Mon Sep 17 00:00:00 2001
From: Mauro <34335419+Velin92@users.noreply.github.com>
Date: Thu, 19 Feb 2026 16:20:47 +0100
Subject: [PATCH] Swift Testing for Unit Tests PART 1 (#5119)
* migrated a lot of unit tests to Swift Testing and added a new implementation for deferred fulfillment
more tests migration
Cleaned the code manually to establish some good patterns
more code improvements
some more code improvements
removed empty tests
update project
* more pr suggestions and cleanups
* removed the TestSetup pattern
* fixing claude not reusing tests
* pr suggestion + added indent rule to swiftformat so that we can prevent AIs to change that
---
.swiftformat | 1 +
ElementX.xcodeproj/project.pbxproj | 44 +--
.../Sources/Other/DeferredFulfillment.swift | 242 ++++++++++++
...hineReadableCodeObjectExtensionsTest.swift | 27 +-
...nalyticsSettingsScreenViewModelTests.swift | 36 +-
UnitTests/Sources/AnalyticsTests.swift | 171 +++++----
.../AppLock/AppLockScreenViewModelTests.swift | 62 +--
.../Sources/AppLock/AppLockServiceTests.swift | 243 ++++++------
.../AppLockSettingsScreenViewModelTests.swift | 25 +-
...kSetupBiometricsScreenViewModelTests.swift | 23 +-
.../AppLockSetupPINScreenViewModelTests.swift | 139 ++++---
.../Sources/AppLock/AppLockTimerTests.swift | 181 ++++-----
.../Sources/AppLock/PINTextFieldTests.swift | 20 +-
.../Sources/AppRouteURLParserTests.swift | 109 +++---
UnitTests/Sources/ArrayTests.swift | 28 +-
.../AttributedStringBuilderTests.swift | 14 +-
UnitTests/Sources/AttributedStringTests.swift | 23 +-
UnitTests/Sources/AudioPlayerStateTests.swift | 198 +++++-----
.../Sources/AudioRecorderStateTests.swift | 44 ++-
UnitTests/Sources/AudioRecorderTests.swift | 16 +-
.../Sources/AuthenticationServiceTests.swift | 67 ++--
...henticationStartScreenViewModelTests.swift | 79 ++--
.../BlockedUsersScreenViewModelTests.swift | 22 +-
.../BugReportScreenViewModelTests.swift | 61 +--
UnitTests/Sources/BugReportServiceTests.swift | 74 ++--
.../Sources/CallScreenViewModelTests.swift | 13 -
.../ChatsTabFlowCoordinatorTests.swift | 2 +-
.../CompletionSuggestionServiceTests.swift | 4 +-
.../ComposerToolbarViewModelTests.swift | 30 +-
UnitTests/Sources/DateTests.swift | 86 +++--
...eactivateAccountScreenViewModelTests.swift | 50 ++-
.../DeclineAndBlockScreenViewModelTests.swift | 50 +--
.../Sources/DeferredFulfillmentTests.swift | 30 +-
...DeveloperOptionsScreenViewModelTests.swift | 13 -
.../EditRoomAddressScreenViewModelTests.swift | 6 +-
.../Sources/ElementCallServiceTests.swift | 168 ++++----
.../EmojiPickerScreenViewModelTests.swift | 38 +-
UnitTests/Sources/EmojiProviderTests.swift | 29 +-
.../Sources/ExpiringTaskRunnerTests.swift | 24 +-
UnitTests/Sources/GeoURITests.swift | 113 +++---
.../GlobalSearchScreenViewModelTests.swift | 44 +--
UnitTests/Sources/HomeScreenRoomTests.swift | 160 ++++----
.../Sources/HomeScreenViewModelTests.swift | 186 ++++-----
.../Sources/InviteUsersViewModelTests.swift | 47 ++-
.../JoinRoomScreenViewModelTests.swift | 2 +-
.../Sources/KeychainControllerTests.swift | 159 ++++----
...nockRequestsListScreenViewModelTests.swift | 88 +++--
...LegalInformationScreenViewModelTests.swift | 13 -
UnitTests/Sources/LocalizationTests.swift | 85 +++--
UnitTests/Sources/LoggingTests.swift | 197 +++++-----
.../Sources/LoginScreenViewModelTests.swift | 171 ++++++---
.../ManageRoomMemberSheetViewModelTests.swift | 36 +-
.../Sources/MapTilerURLBuilderTests.swift | 49 +--
.../Sources/MatrixEntityRegexTests.swift | 88 +++--
.../Sources/MediaPlayerProviderTests.swift | 34 +-
.../MediaProvider/MediaLoaderTests.swift | 36 +-
...diaUploadPreviewScreenViewModelTests.swift | 8 +-
.../MediaUploadingPreprocessorTests.swift | 2 +-
...essageForwardingScreenViewModelTests.swift | 55 ++-
.../NavigationRootCoordinatorTests.swift | 56 +--
.../NavigationStackCoordinatorTests.swift | 112 +++---
.../NavigationTabCoordinatorTests.swift | 131 ++++---
.../NotificationContentBuilderTests.swift | 166 ++++----
UnitTests/Sources/PermalinkTests.swift | 48 +--
UnitTests/Sources/PillContextTests.swift | 101 ++---
.../PinnedEventsBannerStateTests.swift | 135 +++----
.../PollFormScreenViewModelTests.swift | 181 +++++----
.../QRCodeLoginScreenViewModelTests.swift | 122 +++---
UnitTests/Sources/RemotePreferenceTests.swift | 43 ++-
.../ReportContentScreenViewModelTests.swift | 31 +-
.../ReportRoomScreenViewModelTests.swift | 146 +++----
...dUserSendFailureScreenViewModelTests.swift | 66 ++--
UnitTests/Sources/RestorationTokenTests.swift | 71 ++--
...hangePermissionsScreenViewModelTests.swift | 125 +++---
.../RoomChangeRolesScreenViewModelTests.swift | 234 ++++++------
...torySearchScreenScreenViewModelTests.swift | 13 -
.../Sources/RoomEventStringBuilderTests.swift | 30 +-
.../Sources/RoomListFiltersStateTests.swift | 124 +++---
.../RoomMemberDetailsViewModelTests.swift | 203 ++++------
.../RoomMembersFlowCoordinatorTests.swift | 24 +-
.../RoomMembersListScreenViewModelTests.swift | 358 +++++++++---------
UnitTests/Sources/RoomPermissionsTests.swift | 30 +-
...esAndPermissionsScreenViewModelTests.swift | 86 +++--
.../RoomStateEventStringBuilderTests.swift | 66 ++--
.../Sources/RoomSummaryProviderTests.swift | 112 +++---
UnitTests/Sources/RoomSummaryTests.swift | 67 ++--
UnitTests/Sources/RoomTests.swift | 16 +-
...eBackupKeyBackupScreenViewModelTests.swift | 13 -
...goutConfirmationScreenViewModelTests.swift | 84 ++--
...ackupRecoveryKeyScreenViewModelTests.swift | 13 -
.../SecureBackupScreenViewModelTests.swift | 13 -
...verConfigurationScreenViewStateTests.swift | 23 +-
.../ServerSelectionScreenViewModelTests.swift | 107 +++---
.../Sources/SessionDirectoriesTests.swift | 62 +--
...SessionVerificationStateMachineTests.swift | 77 ++--
.../SettingsScreenViewModelTests.swift | 22 +-
.../SoftLogoutScreenViewModelTests.swift | 55 +--
.../SpaceAddRoomsScreenViewModelTests.swift | 67 ++--
.../SpaceListScreenViewModelTests.swift | 144 ++++---
.../Sources/StartChatViewModelTests.swift | 45 ++-
.../StaticLocationScreenViewModelTests.swift | 109 +++---
UnitTests/Sources/StringTests.swift | 97 ++---
.../Sources/TextBasedRoomTimelineTests.swift | 26 +-
.../Sources/TimelineItemFactoryTests.swift | 32 +-
UnitTests/Sources/URLComponentsTests.swift | 86 ++---
UnitTests/Sources/URLTests.swift | 30 +-
UnitTests/Sources/UserAgentBuilderTests.swift | 20 +-
.../UserDetailsEditScreenViewModelTests.swift | 82 ++--
.../UserDiscoveryServiceTest.swift | 78 ++--
.../UserIndicatorControllerTests.swift | 59 +--
UnitTests/Sources/UserPreferenceTests.swift | 78 ++--
.../UserProfileScreenViewModelTests.swift | 50 +--
.../UserSession/UserSessionTests.swift | 12 -
.../UserSessionFlowCoordinatorTests.swift | 190 +++++-----
.../Sources/VoiceMessageCacheTests.swift | 53 +--
.../VoiceMessageMediaManagerTests.swift | 67 ++--
.../SupportingFiles/UnitTests.xctestplan | 1 +
.../Tests/CompoundTests/PreviewTests.swift | 2 +-
118 files changed, 4630 insertions(+), 4129 deletions(-)
create mode 100644 ElementX/Sources/Other/DeferredFulfillment.swift
delete mode 100644 UnitTests/Sources/CallScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/DeveloperOptionsScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/LegalInformationScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/RoomDirectorySearchScreenScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/SecureBackupKeyBackupScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/SecureBackupRecoveryKeyScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/SecureBackupScreenViewModelTests.swift
delete mode 100644 UnitTests/Sources/UserSession/UserSessionTests.swift
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